ArchiveWriter.cpp 79 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "ArchiveWriter.h"
  9. #include <AzCore/IO/ByteContainerStream.h>
  10. #include <AzCore/IO/GenericStreams.h>
  11. #include <AzCore/IO/OpenMode.h>
  12. #include <AzCore/IO/Path/Path.h>
  13. #include <AzCore/Memory/SystemAllocator.h>
  14. #include <AzCore/std/parallel/scoped_lock.h>
  15. #include <AzCore/std/string/conversions.h>
  16. #include <AzCore/Task/TaskGraph.h>
  17. #include <Archive/ArchiveTypeIds.h>
  18. #include <Compression/CompressionInterfaceAPI.h>
  19. #include <Compression/DecompressionInterfaceAPI.h>
  20. namespace Archive
  21. {
  22. // Implement TypeInfo, Rtti and Allocator support
  23. AZ_TYPE_INFO_WITH_NAME_IMPL(ArchiveWriter, "ArchiveWriter", ArchiveWriterTypeId);
  24. AZ_RTTI_NO_TYPE_INFO_IMPL(ArchiveWriter, IArchiveWriter);
  25. AZ_CLASS_ALLOCATOR_IMPL(ArchiveWriter, AZ::SystemAllocator);
  26. ArchiveWriter::ArchiveWriter() = default;
  27. ArchiveWriter::~ArchiveWriter()
  28. {
  29. UnmountArchive();
  30. }
  31. ArchiveWriter::ArchiveWriter(const ArchiveWriterSettings& writerSettings)
  32. : m_settings(writerSettings)
  33. {}
  34. ArchiveWriter::ArchiveWriter(AZ::IO::PathView archivePath, const ArchiveWriterSettings& writerSettings)
  35. : m_settings(writerSettings)
  36. {
  37. MountArchive(archivePath);
  38. }
  39. ArchiveWriter::ArchiveWriter(ArchiveStreamPtr archiveStream, const ArchiveWriterSettings& writerSettings)
  40. : m_settings(writerSettings)
  41. {
  42. MountArchive(AZStd::move(archiveStream));
  43. }
  44. bool ArchiveWriter::ReadArchiveHeader(ArchiveHeader& archiveHeader, AZ::IO::GenericStream& archiveStream)
  45. {
  46. // Read the Archive header into memory
  47. AZStd::scoped_lock archiveLock(m_archiveStreamMutex);
  48. archiveStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  49. AZ::IO::SizeType bytesRead = archiveStream.Read(sizeof(archiveHeader), &archiveHeader);
  50. archiveStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  51. if (bytesRead != sizeof(archiveHeader))
  52. {
  53. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingHeader, ArchiveWriterErrorString::format(
  54. "Archive header should have size %zu, but only %llu bytes were read from the beginning of the archive",
  55. sizeof(archiveHeader), bytesRead) });
  56. }
  57. else if (auto archiveValidationResult = ValidateHeader(archiveHeader);
  58. archiveValidationResult)
  59. {
  60. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingHeader,
  61. archiveValidationResult.m_errorMessage });
  62. }
  63. return true;
  64. }
  65. bool ArchiveWriter::ReadArchiveTOC(ArchiveTableOfContents& archiveToc, AZ::IO::GenericStream& archiveStream,
  66. const ArchiveHeader& archiveHeader)
  67. {
  68. // RAII structure which resets the archive stream to offset 0
  69. // when it goes out of scope
  70. struct SeekStreamToBeginRAII
  71. {
  72. ~SeekStreamToBeginRAII()
  73. {
  74. m_stream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  75. }
  76. AZ::IO::GenericStream& m_stream;
  77. };
  78. if (archiveHeader.m_tocOffset > archiveStream.GetLength())
  79. {
  80. // The TOC offset is invalid since it is after the end of the stream
  81. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents,
  82. ArchiveWriterErrorString::format("TOC offset is invalid. It is pass the end of the stream."
  83. " Offset value %llu, archive stream size %llu",
  84. static_cast<AZ::u64>(archiveHeader.m_tocOffset), archiveStream.GetLength()) });
  85. return false;
  86. }
  87. // Buffer which stores the raw table of contents data from the archive file
  88. AZStd::vector<AZStd::byte> tocBuffer;
  89. // Seek to the location of the Table of Contents
  90. {
  91. AZStd::scoped_lock archiveLock(m_archiveStreamMutex);
  92. // Make sure the archive offset is reset to 0 on return
  93. SeekStreamToBeginRAII seekToBeginScope{ archiveStream };
  94. archiveStream.Seek(archiveHeader.m_tocOffset, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  95. tocBuffer.resize_no_construct(archiveHeader.GetTocStoredSize());
  96. AZ::IO::SizeType bytesRead = archiveStream.Read(tocBuffer.size(), tocBuffer.data());
  97. if (bytesRead != tocBuffer.size())
  98. {
  99. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents,
  100. ArchiveWriterErrorString::format("Unable to read all TOC bytes from the archive."
  101. " The TOC size is %zu, but only %llu bytes were read",
  102. tocBuffer.size(), bytesRead)});
  103. return false;
  104. }
  105. }
  106. // Check if the archive table of contents is compressed
  107. if (archiveHeader.m_tocCompressionAlgoIndex < UncompressedAlgorithmIndex)
  108. {
  109. auto decompressionRegistrar = Compression::DecompressionRegistrar::Get();
  110. if (decompressionRegistrar == nullptr)
  111. {
  112. // The decompression registrar does not exist
  113. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents,
  114. ArchiveWriterErrorString("The Decompression Registry is not available"
  115. " Is the Compression gem active?") });
  116. return false;
  117. }
  118. Compression::CompressionAlgorithmId tocCompressionAlgorithmId =
  119. archiveHeader.m_compressionAlgorithmsIds[archiveHeader.m_tocCompressionAlgoIndex];
  120. Compression::IDecompressionInterface* decompressionInterface =
  121. decompressionRegistrar->FindDecompressionInterface(tocCompressionAlgorithmId);
  122. if (decompressionInterface == nullptr)
  123. {
  124. // Compression algorithm isn't registered with the decompression registrar
  125. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents,
  126. ArchiveWriterErrorString::format("Compression Algorithm %u used by TOC"
  127. " isn't registered with decompression registrar",
  128. AZStd::to_underlying(tocCompressionAlgorithmId)) });
  129. return false;
  130. }
  131. // Resize the uncompressed TOC buffer to be the size of the uncompressed Table of Contents
  132. AZStd::vector<AZStd::byte> uncompressedTocBuffer;
  133. uncompressedTocBuffer.resize_no_construct(archiveHeader.GetUncompressedTocSize());
  134. // Run the compressed toc data through the decompressor
  135. if (Compression::DecompressionResultData decompressionResultData =
  136. decompressionInterface->DecompressBlock(uncompressedTocBuffer, tocBuffer, Compression::DecompressionOptions{});
  137. decompressionResultData)
  138. {
  139. // If decompression succeed, move the uncompressed buffer to the tocBuffer variable
  140. tocBuffer = AZStd::move(uncompressedTocBuffer);
  141. if (decompressionResultData.GetUncompressedByteCount() != tocBuffer.size())
  142. {
  143. // The size of uncompressed size of the data does not match the total uncompressed
  144. // TOC size read from the ArchiveHeader::GetUncompressedTocSize() function
  145. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents,
  146. ArchiveWriterErrorString::format("The uncompressed TOC size %llu does not match the total uncompressed size %zu"
  147. " read from the archive header",
  148. decompressionResultData.GetUncompressedByteCount(), tocBuffer.size()) });
  149. return false;
  150. }
  151. }
  152. }
  153. // Map the Table of Contents to a structure that makes it easier to maintain
  154. // the Archive TOC state in memory and allows adding to the existing tables
  155. if (auto tocView = ArchiveTableOfContentsView::CreateFromArchiveHeaderAndBuffer(archiveHeader, tocBuffer);
  156. tocView)
  157. {
  158. if (auto archiveTocCreateOutcome = ArchiveTableOfContents::CreateFromTocView(tocView.value());
  159. archiveTocCreateOutcome)
  160. {
  161. archiveToc = AZStd::move(archiveTocCreateOutcome.value());
  162. }
  163. else
  164. {
  165. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents, archiveTocCreateOutcome.error() });
  166. return false;
  167. }
  168. }
  169. else
  170. {
  171. // Invoke the error callback indicating an error reading the table of contents
  172. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorReadingTableOfContents, tocView.error().m_errorMessage });
  173. return false;
  174. }
  175. return true;
  176. }
  177. bool ArchiveWriter::BuildDeletedFileBlocksMap(const ArchiveHeader& archiveHeader,
  178. AZ::IO::GenericStream& archiveStream)
  179. {
  180. // Build the deleted block map using the the first deleted block offset
  181. // in the Archive Header
  182. AZ::u64 nextBlock{ DeletedBlockOffsetSentinel };
  183. for (AZ::u64 deletedBlockOffset = archiveHeader.m_firstDeletedBlockOffset;
  184. deletedBlockOffset != DeletedBlockOffsetSentinel; deletedBlockOffset = nextBlock)
  185. {
  186. // initialize the next block value to the deleted block offset sentinel value
  187. // of 0xffff'ffff'ffff'ffff
  188. if (archiveStream.Read(sizeof(nextBlock), &nextBlock) != sizeof(nextBlock))
  189. {
  190. // If the next block offset cannot be read in force the deletedBlockOffset
  191. // to be the deleted block offset sentinel value to exit the loop
  192. nextBlock = DeletedBlockOffsetSentinel;
  193. }
  194. // Read in the size of the deleted block
  195. if (AZ::u64 blockSize{};
  196. archiveStream.Read(sizeof(blockSize), &blockSize) == sizeof(blockSize))
  197. {
  198. // If the block size has been successfully read, update the deleted block offset map
  199. // with a key of the block size which maps to a sorted set which contains
  200. // the current iterated block
  201. // Make sure any block size is aligned UP to a 512-byte boundary
  202. // and any block offset is aligned UP to a 512-byte boundary
  203. // This prevents issues with writing block data to a non-aligned block
  204. AZ::u64 alignedBlockSize = AZ_SIZE_ALIGN_UP(blockSize, ArchiveDefaultBlockAlignment);
  205. AZ::u64 alignedBlockOffset = AZ_SIZE_ALIGN_UP(deletedBlockOffset, ArchiveDefaultBlockAlignment);
  206. if (alignedBlockSize > 0)
  207. {
  208. auto& deletedBlockOffsetSet = m_deletedBlockSizeToOffsetMap[alignedBlockSize];
  209. deletedBlockOffsetSet.emplace(alignedBlockOffset);
  210. }
  211. }
  212. }
  213. MergeContiguousDeletedBlocks();
  214. return true;
  215. }
  216. void ArchiveWriter::MergeContiguousDeletedBlocks()
  217. {
  218. AZStd::map<AZ::u64, AZ::u64> deletedBlockOffsetRangeMap;
  219. for (auto deletedBlockIt = m_deletedBlockSizeToOffsetMap.begin(); deletedBlockIt != m_deletedBlockSizeToOffsetMap.end(); ++deletedBlockIt)
  220. {
  221. const AZ::u64 deletedBlockSize = deletedBlockIt->first;
  222. auto& deletedBlockOffsetSet = deletedBlockIt->second;
  223. for (const AZ::u64 deletedBlockOffset : deletedBlockOffsetSet)
  224. {
  225. // Stores an iterator to the block offset range to update
  226. auto updateBlockRangeIter = deletedBlockOffsetRangeMap.end();
  227. // Locate the last block before the block offset
  228. auto nextBlockRangeIt = deletedBlockOffsetRangeMap.lower_bound(deletedBlockOffset);
  229. if (nextBlockRangeIt != deletedBlockOffsetRangeMap.begin())
  230. {
  231. // Check if the block range entry for the previous block
  232. // ends at the beginning of the current block
  233. if (auto previousBlockRangeIt = --nextBlockRangeIt;
  234. previousBlockRangeIt->second == deletedBlockOffset)
  235. {
  236. updateBlockRangeIter = previousBlockRangeIt;
  237. // Update the entry for the previous block offset range
  238. // to have its seconded value point to the end of the current block
  239. updateBlockRangeIter->second += deletedBlockSize;
  240. }
  241. }
  242. typename decltype(deletedBlockOffsetRangeMap)::node_type nextBlockRangeNodeHandle;
  243. // if the iterator is before the end it represents the next block offset range to check
  244. // the condition is if it's begin offset is at end of the current deleted block
  245. if (nextBlockRangeIt != deletedBlockOffsetRangeMap.end()
  246. && nextBlockRangeIt->first == deletedBlockSize + deletedBlockSize)
  247. {
  248. // Extract the next entry out of the block offset range map
  249. nextBlockRangeNodeHandle = deletedBlockOffsetRangeMap.extract(nextBlockRangeIt);
  250. }
  251. // There are four different scenarios here one of which has already been taken care of "last block before"
  252. // logic above
  253. // 1. The deleted block exist between two existing deleted block offset range entries
  254. // i.e <current block metadata> = (offset = 2 MiB, size = 2 MiB)
  255. // <entry 1> = (0-2) MiB
  256. // <entry 2> = (4-6) MiB
  257. // In this case the number of entries should be collapsed to 1 by
  258. // <entry 1> = (0-6) MiB
  259. // This can be done by updating <entry 1> end range to be <entry 2> end range and removing <entry 2>
  260. //
  261. // 2. The deleted block exist after another deleted block, but the next block is in use
  262. // i.e <current block metadata> = (offset = 2 MiB, size = 2 MiB)
  263. // <entry 1> = (0-2) MiB
  264. // In this case the existing entry is updated, to increment the current block size
  265. // <entry 1> = (0-4) MiB
  266. // NOTE: This was already done up above by updating updateBlockRange iterator
  267. //
  268. // 3. The deleted block exist before another deleted block, but the previous block is in use
  269. // i.e <current block metadata> = (offset = 2 MiB, size = 2 MiB)
  270. // <entry 1> = (4-6) MiB
  271. // In this case the next block offset range node handle is extracted from the block range offset map,
  272. // its key is updated to be the current block offset and finally that node handle is re-inserted
  273. // back into the block range offset map
  274. // <entry 1> = (2-6) MiB
  275. //
  276. // 4. The deleted block is not surrounded by any deleted blocks
  277. // i.e <current block metadata> = (offset = 2 MiB, size = 2 MiB)
  278. // In this case, a new entry is added with the current block offset with an end of that is its offset + range
  279. // <entry 1> = (2-4) MiB
  280. // Scenario 1
  281. if (updateBlockRangeIter != deletedBlockOffsetRangeMap.end()
  282. && nextBlockRangeNodeHandle)
  283. {
  284. // Updated the existing prev block range iterator end entry
  285. updateBlockRangeIter->second = nextBlockRangeNodeHandle.mapped();
  286. }
  287. // Scenario 2
  288. else if (updateBlockRangeIter != deletedBlockOffsetRangeMap.end())
  289. {
  290. // No-op - Already handled at the beginning of the for loop
  291. }
  292. // Scenario 3
  293. else if (nextBlockRangeNodeHandle)
  294. {
  295. // Update the beginning of the next block range node handle and reinsert it
  296. nextBlockRangeNodeHandle.key() = deletedBlockOffset;
  297. deletedBlockOffsetRangeMap.insert(AZStd::move(nextBlockRangeNodeHandle));
  298. }
  299. // Scenario 4
  300. else
  301. {
  302. // Insert a new element
  303. deletedBlockOffsetRangeMap.emplace(deletedBlockOffset, deletedBlockOffset + deletedBlockSize);
  304. }
  305. }
  306. }
  307. // Now create a local block size -> block offset set by iterating over the
  308. // deleted block offset range map
  309. decltype(m_deletedBlockSizeToOffsetMap) mergeDeletedBlockSizeToOffsetMap;
  310. for (const auto& [deletedBlockFirst, deletedBlockLast] : deletedBlockOffsetRangeMap)
  311. {
  312. // Create/update the block offset set by using the difference in offset values as the deleted block size
  313. auto& mergeDeletedBlockOffsetSet = mergeDeletedBlockSizeToOffsetMap[deletedBlockLast - deletedBlockFirst];
  314. // Add the block offset first to the set
  315. mergeDeletedBlockOffsetSet.emplace(deletedBlockFirst);
  316. }
  317. // Swap the local block size -> block offset set with the member variable
  318. m_deletedBlockSizeToOffsetMap.swap(mergeDeletedBlockSizeToOffsetMap);
  319. }
  320. bool ArchiveWriter::BuildFilePathMap(const ArchiveTableOfContents& archiveToc)
  321. {
  322. // Clear any old entries from the path map
  323. m_pathMap.clear();
  324. // Build a map of file path to the index offset within the Archive TOC
  325. for (size_t filePathIndex{}; filePathIndex < archiveToc.m_filePaths.size(); ++filePathIndex)
  326. {
  327. m_pathMap.emplace(archiveToc.m_filePaths[filePathIndex], filePathIndex);
  328. }
  329. return true;
  330. }
  331. bool ArchiveWriter::MountArchive(AZ::IO::PathView archivePath)
  332. {
  333. UnmountArchive();
  334. AZ::IO::FixedMaxPath mountPath{ archivePath };
  335. constexpr AZ::IO::OpenMode openMode =
  336. AZ::IO::OpenMode::ModeCreatePath
  337. | AZ::IO::OpenMode::ModeAppend
  338. | AZ::IO::OpenMode::ModeUpdate;
  339. m_archiveStream.reset(aznew AZ::IO::SystemFileStream(mountPath.c_str(), openMode));
  340. // Early return if the archive is not open
  341. if (!m_archiveStream->IsOpen())
  342. {
  343. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorOpeningArchive, ArchiveWriterErrorString::format(
  344. "Archive with filename %s could not be open",
  345. mountPath.c_str()) });
  346. return false;
  347. }
  348. if (!ReadArchiveHeaderAndToc())
  349. {
  350. // UnmountArchive is invoked to reset
  351. // the Archive Header, TOC,
  352. // file path -> file index map and entries
  353. // and the deleted block offset table
  354. UnmountArchive();
  355. return false;
  356. }
  357. return true;
  358. }
  359. bool ArchiveWriter::MountArchive(ArchiveStreamPtr archiveStream)
  360. {
  361. UnmountArchive();
  362. m_archiveStream = AZStd::move(archiveStream);
  363. if (m_archiveStream == nullptr || !m_archiveStream->IsOpen())
  364. {
  365. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorOpeningArchive,
  366. ArchiveWriterErrorString("Archive stream pointer is nullptr or not open") });
  367. return false;
  368. }
  369. if (!ReadArchiveHeaderAndToc())
  370. {
  371. // UnmountArchive is invoked to reset
  372. // the Archive Header, TOC,
  373. // file path -> file index map and entries
  374. // and the deleted block offset table
  375. UnmountArchive();
  376. return false;
  377. }
  378. return true;
  379. }
  380. bool ArchiveWriter::ReadArchiveHeaderAndToc()
  381. {
  382. if (m_archiveStream == nullptr)
  383. {
  384. return false;
  385. }
  386. // An empty file is valid to use for writing a new archive therefore return true
  387. if (m_archiveStream->GetLength() == 0)
  388. {
  389. return true;
  390. }
  391. const bool mountResult = ReadArchiveHeader(m_archiveHeader, *m_archiveStream)
  392. && ReadArchiveTOC(m_archiveToc, *m_archiveStream, m_archiveHeader)
  393. && BuildDeletedFileBlocksMap(m_archiveHeader, *m_archiveStream)
  394. && BuildFilePathMap(m_archiveToc);
  395. return mountResult;
  396. }
  397. void ArchiveWriter::UnmountArchive()
  398. {
  399. if (m_archiveStream != nullptr && m_archiveStream->IsOpen())
  400. {
  401. if (CommitResult commitResult = Commit();
  402. !commitResult)
  403. {
  404. // Signal the error callback if the commit operation fails
  405. m_settings.m_errorCallback({ ArchiveWriterErrorCode::ErrorWritingTableOfContents,
  406. AZStd::move(commitResult.error()) });
  407. }
  408. // Clear out the path map, the archive TOC and the archive header
  409. // deleted block size -> raw file offset
  410. // and the removed file index into TOC vector set
  411. // on unmount
  412. m_pathMap.clear();
  413. m_removedFileIndices.clear();
  414. m_deletedBlockSizeToOffsetMap.clear();
  415. m_archiveToc = {};
  416. m_archiveHeader = {};
  417. }
  418. m_archiveStream.reset();
  419. }
  420. bool ArchiveWriter::IsMounted() const
  421. {
  422. return m_archiveStream != nullptr && m_archiveStream->IsOpen();
  423. }
  424. auto ArchiveWriter::Commit() -> CommitResult
  425. {
  426. if (m_archiveToc.m_filePaths.size() != m_archiveToc.m_fileMetadataTable.size())
  427. {
  428. CommitResult result;
  429. result = AZStd::unexpected(ResultString::format("The Archive TOC of contents has a mismatched count of "
  430. " file paths (size=%zu) and the file metadata entries (size=%zu).\n"
  431. "Cannot commit archive.",
  432. m_archiveToc.m_filePaths.size(), m_archiveToc.m_fileMetadataTable.size()));
  433. return result;
  434. }
  435. if (AZStd::scoped_lock archiveLock(m_archiveStreamMutex);
  436. !IsMounted())
  437. {
  438. CommitResult result;
  439. result = AZStd::unexpected(ResultString::format("The stream to commit the archive data is not mounted.\n"
  440. "Cannot commit archive."));
  441. return result;
  442. }
  443. // Update the Archive uncompressed TOC file sizes
  444. m_archiveHeader.m_tocFileMetadataTableUncompressedSize = 0;
  445. m_archiveHeader.m_tocPathIndexTableUncompressedSize = 0;
  446. m_archiveHeader.m_tocPathBlobUncompressedSize = 0;
  447. for (size_t fileIndex{}; fileIndex < m_archiveToc.m_filePaths.size(); ++fileIndex)
  448. {
  449. const ArchiveTableOfContents::Path& filePath = m_archiveToc.m_filePaths[fileIndex];
  450. const ArchiveTocFileMetadata& fileMetadata = m_archiveToc.m_fileMetadataTable[fileIndex];
  451. // An empty file path represents a removed file from the archive, so skip it
  452. if (!filePath.empty())
  453. {
  454. m_archiveHeader.m_tocFileMetadataTableUncompressedSize += sizeof(fileMetadata);
  455. // The ArchiveTocFilePathIndex isn't stored in the Table of Contents directly
  456. // It is composed later in this function
  457. // That is why sizeof is being used on a struct instead of a member
  458. m_archiveHeader.m_tocPathIndexTableUncompressedSize += sizeof(ArchiveTocFilePathIndex);
  459. m_archiveHeader.m_tocPathBlobUncompressedSize += static_cast<AZ::u32>(filePath.Native().size());
  460. }
  461. }
  462. // 1. Write out any deleted blocks to the Archive file if there are any
  463. if (!m_deletedBlockSizeToOffsetMap.empty())
  464. {
  465. // First merge any contiguous deleted blocks into a single deleted block entry
  466. MergeContiguousDeletedBlocks();
  467. // As the deleted blockSizeToOffsetMap is keyed on block size
  468. // The deleted offset linked list would store blocks that are increasingly large in size
  469. // That is not a problem, however what could be a problem is that the deleted block offsets
  470. // are most likely not sequential and that could make reading the archive take longer
  471. // when building the deleted block table the next time the archive writer is used
  472. // therefore a set of deleted block offsets are created and then written
  473. // to the deleted blocks within the offset in file order
  474. struct SortByOffset
  475. {
  476. constexpr bool operator()(const BlockOffsetSizePair& left, const BlockOffsetSizePair& right) const
  477. {
  478. return left.m_offset < right.m_offset;
  479. }
  480. };
  481. AZStd::set<BlockOffsetSizePair, SortByOffset> m_deletedBlockOffsetSet;
  482. // Populated the sorted deleted block offset set
  483. for (const auto& [blockSize, blockOffsetSet] : m_deletedBlockSizeToOffsetMap)
  484. {
  485. for (const AZ::u64 blockOffset : blockOffsetSet)
  486. {
  487. m_deletedBlockOffsetSet.insert({ blockOffset, blockSize });
  488. }
  489. }
  490. // Update the first deleted block offset entry to point to the beginning of the block offset set
  491. m_archiveHeader.m_firstDeletedBlockOffset = m_deletedBlockOffsetSet.begin()->m_offset;
  492. // Iterate over the deleted block offset and write the block offset and block size to the first 16-bytes in the block
  493. // It is guaranteed that any deleted block has at least a size of 512 due to the ArchiveDefaultBlockAlignment
  494. AZStd::scoped_lock archiveLock(m_archiveStreamMutex);
  495. for (auto currentDeletedBlockIt = m_deletedBlockOffsetSet.begin(); currentDeletedBlockIt != m_deletedBlockOffsetSet.end();
  496. ++currentDeletedBlockIt)
  497. {
  498. // Seek to the current deleted block offset and write the next deleted block offset value and the size
  499. // of the current deleted block
  500. m_archiveStream->Seek(currentDeletedBlockIt->m_offset, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  501. if (auto nextDeletedBlockIt = AZStd::next(currentDeletedBlockIt);
  502. nextDeletedBlockIt != m_deletedBlockOffsetSet.end())
  503. {
  504. m_archiveStream->Write(sizeof(nextDeletedBlockIt->m_offset), &nextDeletedBlockIt->m_offset);
  505. }
  506. else
  507. {
  508. // For the final block write the deleted block offset sentinel value of 0xffff'ffff'ffff'ffff
  509. m_archiveStream->Write(sizeof(DeletedBlockOffsetSentinel), &DeletedBlockOffsetSentinel);
  510. }
  511. m_archiveStream->Write(sizeof(currentDeletedBlockIt->m_size), &currentDeletedBlockIt->m_size);
  512. }
  513. }
  514. // Update the Archive uncompressed TOC block offset table size
  515. // As removed files block offset table entries aren't removed from the archive
  516. // It can be calculated using multiplication of <number of block line entries> * sizeof(ArchiveBlockLineUnion)
  517. // The simplest approach is to convert it to a span and use the size_byte function on it
  518. m_archiveHeader.m_tocBlockOffsetTableUncompressedSize = static_cast<AZ::u32>(
  519. AZStd::span(m_archiveToc.m_blockOffsetTable).size_bytes());
  520. // 2. Write the Archive Table of Contents
  521. // Both buffers lifetime must be encompass the tocWriteSpan below
  522. // to make sure the span points to a valid buffer
  523. AZStd::vector<AZStd::byte> tocRawBuffer;
  524. AZStd::vector<AZStd::byte> tocCompressBuffer;
  525. WriteTocRawResult rawTocResult = WriteTocRaw(tocRawBuffer);
  526. if (!rawTocResult)
  527. {
  528. CommitResult result;
  529. result = AZStd::unexpected(AZStd::move(rawTocResult.m_errorString));
  530. return result;
  531. }
  532. // Initialize the tocWriteSpan to the tocRawBuffer above
  533. AZStd::span<const AZStd::byte> tocWriteSpan = rawTocResult.m_tocSpan;
  534. // Check if the table of contents should be compressed
  535. Compression::CompressionAlgorithmId tocCompressionAlgorithmId =
  536. m_archiveHeader.m_tocCompressionAlgoIndex < UncompressedAlgorithmIndex
  537. ? m_archiveHeader.m_compressionAlgorithmsIds[m_archiveHeader.m_tocCompressionAlgoIndex]
  538. : Compression::Uncompressed;
  539. if (m_settings.m_tocCompressionAlgorithm.has_value())
  540. {
  541. tocCompressionAlgorithmId = m_settings.m_tocCompressionAlgorithm.value();
  542. }
  543. if (tocCompressionAlgorithmId != Compression::Invalid && tocCompressionAlgorithmId != Compression::Uncompressed)
  544. {
  545. CompressTocRawResult compressResult = CompressTocRaw(tocCompressBuffer, tocRawBuffer,
  546. tocCompressionAlgorithmId);
  547. if (!compressResult)
  548. {
  549. CommitResult result;
  550. result = AZStd::unexpected(AZStd::move(compressResult.m_errorString));
  551. return result;
  552. }
  553. // The tocWriteSpan now points to the tocCompressBufer
  554. // via the CompressTocRawResult span
  555. tocWriteSpan = compressResult.m_compressedTocSpan;
  556. // Update the archive header compressed toc metadata
  557. m_archiveHeader.m_tocCompressedSize = static_cast<AZ::u32>(tocWriteSpan.size());
  558. m_archiveHeader.m_tocCompressionAlgoIndex = static_cast<AZ::u32>(FindCompressionAlgorithmId(
  559. tocCompressionAlgorithmId, m_archiveHeader));
  560. }
  561. else
  562. {
  563. // The table of contents is not compressed
  564. // so store a size of 0
  565. m_archiveHeader.m_tocCompressedSize = 0;
  566. }
  567. // Performs writing of the raw table of contents table
  568. if (!tocWriteSpan.empty())
  569. {
  570. AZStd::scoped_lock archiveLock(m_archiveStreamMutex);
  571. // Seek to the Table of Contents toc offset index
  572. m_archiveStream->Seek(m_archiveHeader.m_tocOffset, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  573. m_archiveStream->Write(tocWriteSpan.size(), tocWriteSpan.data());
  574. }
  575. // 3. Write out the updated Archive Header
  576. {
  577. AZStd::scoped_lock archiveLock(m_archiveStreamMutex);
  578. // Seek to the beginning of the stream and write the archive header overtop any previous header
  579. m_archiveStream->Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  580. if (size_t writeSize = m_archiveStream->Write(sizeof(m_archiveHeader), &m_archiveHeader);
  581. writeSize != sizeof(m_archiveHeader))
  582. {
  583. CommitResult result;
  584. result = AZStd::unexpected(ResultString::format("Failed to write Archive Header to the beginning of stream.\n"
  585. "Cannot commit archive."));
  586. return result;
  587. }
  588. // Write padding bytes to stream until the 512-byte alignment is reached
  589. AZStd::array<AZStd::byte, ArchiveDefaultBlockAlignment - sizeof(m_archiveHeader)> headerPadding{};
  590. m_archiveStream->Write(headerPadding.size(), headerPadding.data());
  591. }
  592. return CommitResult{};
  593. }
  594. ArchiveWriter::WriteTocRawResult::operator bool() const
  595. {
  596. return m_errorString.empty();
  597. }
  598. auto ArchiveWriter::WriteTocRaw(AZStd::vector<AZStd::byte>& tocOutputBuffer) -> WriteTocRawResult
  599. {
  600. tocOutputBuffer.reserve(m_archiveHeader.GetUncompressedTocSize());
  601. AZ::IO::ByteContainerStream tocOutputStream(&tocOutputBuffer);
  602. tocOutputStream.Write(sizeof(ArchiveTocMagicBytes), &ArchiveTocMagicBytes);
  603. // Write padding bytes to ensure that the file metadata entries start on a 32-byte boundary
  604. AZStd::byte fileMetadataAlignmentBytes[sizeof(ArchiveTocFileMetadata) - sizeof(ArchiveTocMagicBytes)]{};
  605. tocOutputStream.Write(AZStd::size(fileMetadataAlignmentBytes), fileMetadataAlignmentBytes);
  606. // Write out the file metadata table first to the table of contents
  607. // The m_fileMetadataTable and m_filePaths should have the same size
  608. for (size_t fileIndex = 0; fileIndex < m_archiveToc.m_filePaths.size(); ++fileIndex)
  609. {
  610. if (!m_archiveToc.m_filePaths.empty())
  611. {
  612. const ArchiveTocFileMetadata& fileMetadata = m_archiveToc.m_fileMetadataTable[fileIndex];
  613. tocOutputStream.Write(sizeof(fileMetadata), &fileMetadata);
  614. }
  615. }
  616. // Write out each file path index table entry
  617. // They are created on the fly here
  618. AZ::u64 filePathOffset{};
  619. for (size_t fileIndex = 0; fileIndex < m_archiveToc.m_filePaths.size(); ++fileIndex)
  620. {
  621. if (!m_archiveToc.m_filePaths.empty())
  622. {
  623. ArchiveTocFilePathIndex filePathIndex;
  624. filePathIndex.m_size = m_archiveToc.m_filePaths[fileIndex].Native().size();
  625. filePathIndex.m_offset = filePathOffset;
  626. // Move the file path table offset forward by the size of the path
  627. filePathOffset += filePathIndex.m_size;
  628. tocOutputStream.Write(sizeof(filePathIndex), &filePathIndex);
  629. }
  630. }
  631. // Write out the file path blob table
  632. // Consecutive paths are not separated by null terminating characters
  633. for (size_t fileIndex = 0; fileIndex < m_archiveToc.m_filePaths.size(); ++fileIndex)
  634. {
  635. if (!m_archiveToc.m_filePaths.empty())
  636. {
  637. // Write path from the AZ::IO::Path
  638. tocOutputStream.Write(m_archiveToc.m_filePaths[fileIndex].Native().size(),
  639. m_archiveToc.m_filePaths[fileIndex].c_str());
  640. }
  641. }
  642. // If the file path blob is not aligned on a 8 byte boundary
  643. // then write 0 bytes until it is aligned
  644. constexpr AZ::u64 FilePathBlobAlignment = 8;
  645. if (const AZ::u64 filePathCurAlignment = filePathOffset % FilePathBlobAlignment;
  646. filePathCurAlignment > 0)
  647. {
  648. // Fill an array of size 8 with '\0' bytes
  649. AZStd::byte paddingBytes[FilePathBlobAlignment]{};
  650. // Writes the remaining bytes to pad to an 8 byte boundary
  651. tocOutputStream.Write(FilePathBlobAlignment - filePathCurAlignment, paddingBytes);
  652. }
  653. // Write out the block offset table
  654. // Since it can contain entries to deleted block lines, the logic is simple here
  655. // It is just the size of the table * sizeof(ArchiveBlockLineUnion)
  656. AZStd::span<ArchiveBlockLineUnion> blockOffsetTableView = m_archiveToc.m_blockOffsetTable;
  657. tocOutputStream.Write(blockOffsetTableView.size_bytes(), blockOffsetTableView.data());
  658. WriteTocRawResult result;
  659. result.m_tocSpan = tocOutputBuffer;
  660. return result;
  661. }
  662. ArchiveWriter::CompressTocRawResult::operator bool() const
  663. {
  664. return m_errorString.empty();
  665. }
  666. auto ArchiveWriter::CompressTocRaw(AZStd::vector<AZStd::byte>& tocCompressionBuffer,
  667. AZStd::span<const AZStd::byte> uncompressedTocInputSpan,
  668. Compression::CompressionAlgorithmId compressionAlgorithmId) -> CompressTocRawResult
  669. {
  670. CompressTocRawResult result;
  671. // Initialize the compressedTocSpan to the uncompressed data
  672. result.m_compressedTocSpan = uncompressedTocInputSpan;
  673. auto compressionRegistrar = Compression::CompressionRegistrar::Get();
  674. if (compressionRegistrar == nullptr)
  675. {
  676. result.m_errorString = "Compression Registrar is not available to compress the raw Table of Contents data";
  677. return result;
  678. }
  679. Compression::ICompressionInterface* compressionInterface =
  680. compressionRegistrar->FindCompressionInterface(compressionAlgorithmId);
  681. if (compressionInterface == nullptr)
  682. {
  683. result.m_errorString = ResultString::format(
  684. "Compression algorithm with ID %u is not registered with the Compression Registrar",
  685. AZStd::to_underlying(compressionAlgorithmId));
  686. return result;
  687. }
  688. // Add the Compression Algorithm ID to the archive header compression algorithm array
  689. AddCompressionAlgorithmId(compressionAlgorithmId, m_archiveHeader);
  690. // Resize the TOC compression Buffer to be able to fit the compressed content
  691. tocCompressionBuffer.resize_no_construct(compressionInterface->CompressBound(uncompressedTocInputSpan.size()));
  692. if (auto compressionResultData = compressionInterface->CompressBlock(
  693. tocCompressionBuffer, uncompressedTocInputSpan);
  694. compressionResultData)
  695. {
  696. result.m_compressedTocSpan = compressionResultData.m_compressedBuffer;
  697. }
  698. else
  699. {
  700. result.m_errorString = compressionResultData.m_compressionOutcome.m_resultString;
  701. }
  702. return result;
  703. }
  704. ArchiveAddFileResult ArchiveWriter::AddFileToArchive(AZ::IO::GenericStream& inputStream,
  705. const ArchiveWriterFileSettings& fileSettings)
  706. {
  707. AZStd::vector<AZStd::byte> fileData;
  708. fileData.resize_no_construct(inputStream.GetLength());
  709. AZ::IO::SizeType bytesRead = inputStream.Read(fileData.size(), fileData.data());
  710. // Unable to read entire stream data into memory
  711. if (bytesRead != fileData.size())
  712. {
  713. ArchiveAddFileResult errorResult;
  714. errorResult.m_relativeFilePath = fileSettings.m_relativeFilePath;
  715. errorResult.m_compressionAlgorithm = fileSettings.m_compressionAlgorithm;
  716. errorResult.m_resultOutcome = AZStd::unexpected(
  717. ResultString::format(R"(Unable to successfully read all bytes(%zu) from input stream.)"
  718. " %llu bytes were read.",
  719. fileData.size(), bytesRead));
  720. return errorResult;
  721. }
  722. return AddFileToArchive(fileData, fileSettings);
  723. }
  724. ArchiveAddFileResult ArchiveWriter::AddFileToArchive(AZStd::span<const AZStd::byte> inputSpan,
  725. const ArchiveWriterFileSettings& fileSettings)
  726. {
  727. if (fileSettings.m_relativeFilePath.empty())
  728. {
  729. ArchiveAddFileResult errorResult;
  730. errorResult.m_compressionAlgorithm = fileSettings.m_compressionAlgorithm;
  731. errorResult.m_resultOutcome = AZStd::unexpected(
  732. ResultString(R"(The file path is empty. File will not be added to the archive.)"));
  733. return errorResult;
  734. }
  735. // Update the file case based on the ArchiveFilePathCase enum
  736. AZ::IO::Path filePath(fileSettings.m_relativeFilePath);
  737. switch (fileSettings.m_fileCase)
  738. {
  739. case ArchiveFilePathCase::Lowercase:
  740. AZStd::to_lower(filePath.Native());
  741. break;
  742. case ArchiveFilePathCase::Uppercase:
  743. AZStd::to_upper(filePath.Native());
  744. break;
  745. }
  746. // Check if a file being added is already in the archive
  747. // If the ArchiveWriterFileMode is set to only add new files
  748. // return an ArchiveAddFileResult with an invalid file token
  749. if (fileSettings.m_fileMode == ArchiveWriterFileMode::AddNew
  750. && ContainsFile(filePath))
  751. {
  752. ArchiveAddFileResult errorResult;
  753. errorResult.m_relativeFilePath = AZStd::move(filePath);
  754. errorResult.m_compressionAlgorithm = fileSettings.m_compressionAlgorithm;
  755. errorResult.m_resultOutcome = AZStd::unexpected(
  756. ResultString::format(R"(The file with relative path "%s" already exist in the archive.)"
  757. " The FileMode::AddNew option was specified.",
  758. errorResult.m_relativeFilePath.c_str()));
  759. return errorResult;
  760. }
  761. ArchiveAddFileResult result;
  762. // Supply the file path with the case changed
  763. result.m_relativeFilePath = AZStd::move(filePath);
  764. // Storage buffer used to store the file data if it s compressed
  765. // It's lifetime must outlive the CompressContentOutcome
  766. AZStd::vector<AZStd::byte> compressionBuffer;
  767. CompressContentOutcome compressOutcome = CompressContentFileAsync(compressionBuffer, fileSettings, inputSpan);
  768. if (!compressOutcome)
  769. {
  770. result.m_compressionAlgorithm = fileSettings.m_compressionAlgorithm;
  771. result.m_resultOutcome = AZStd::unexpected(AZStd::move(compressOutcome.error()));
  772. return result;
  773. }
  774. // Populate the compression algorithm used in the result structure
  775. if (compressOutcome->m_compressionAlgorithmIndex < m_archiveHeader.m_compressionAlgorithmsIds.size())
  776. {
  777. result.m_compressionAlgorithm = m_archiveHeader.m_compressionAlgorithmsIds[compressOutcome->m_compressionAlgorithmIndex];
  778. }
  779. // Update the archive stream
  780. ContentFileData contentFileData;
  781. contentFileData.m_relativeFilePath = result.m_relativeFilePath;
  782. contentFileData.m_uncompressedSpan = inputSpan;
  783. contentFileData.m_contentFileBlocks = AZStd::move(*compressOutcome);
  784. // Write the file content to the archive stream and store the archive file path token
  785. // which is used to lookup the file for removal
  786. result.m_filePathToken = WriteContentFileToArchive(fileSettings, contentFileData);
  787. return result;
  788. }
  789. auto ArchiveWriter::CompressContentFileAsync(AZStd::vector<AZStd::byte>& compressionDataBuffer,
  790. const ArchiveWriterFileSettings& fileSettings,
  791. AZStd::span<const AZStd::byte> inputDataSpan) -> CompressContentOutcome
  792. {
  793. // If the file is empty, there is nothing to compress
  794. if (inputDataSpan.empty())
  795. {
  796. return ContentFileBlocks{};
  797. }
  798. // Try to register the compression algorithm id with the Archive Header compression algorithm id array
  799. // if has not already been registered
  800. AddCompressionAlgorithmId(fileSettings.m_compressionAlgorithm, m_archiveHeader);
  801. // Now lookup the compression algorithm id to make sure it corresponds to a valid entry
  802. // in the compression algorithm id array
  803. size_t compressionAlgorithmIndex = FindCompressionAlgorithmId(fileSettings.m_compressionAlgorithm, m_archiveHeader);
  804. // If a valid compression algorithm Id is not found in the compression algorithm id array
  805. // then an invalid file token is returned
  806. if (compressionAlgorithmIndex == InvalidAlgorithmIndex)
  807. {
  808. AZStd::unexpected<ResultString> errorString = AZStd::unexpected(
  809. ResultString::format(R"(Unable to locate compression algorithm registered with id %u in the archive.)",
  810. static_cast<AZ::u32>(fileSettings.m_compressionAlgorithm)));
  811. return errorString;
  812. }
  813. ContentFileBlocks contentFileBlocks;
  814. contentFileBlocks.m_writeSpan = inputDataSpan;
  815. if (compressionAlgorithmIndex >= UncompressedAlgorithmIndex)
  816. {
  817. contentFileBlocks.m_blockOffsetSizePairs = AZStd::vector<BlockOffsetSizePair>{ { 0, inputDataSpan.size() } };
  818. return contentFileBlocks;
  819. }
  820. auto compressionRegistrar = Compression::CompressionRegistrar::Get();
  821. if (compressionRegistrar == nullptr)
  822. {
  823. contentFileBlocks.m_blockOffsetSizePairs = AZStd::vector<BlockOffsetSizePair>{ { 0, inputDataSpan.size() } };
  824. return contentFileBlocks;
  825. }
  826. Compression::ICompressionInterface* compressionInterface =
  827. compressionRegistrar->FindCompressionInterface(fileSettings.m_compressionAlgorithm);
  828. if (compressionInterface == nullptr)
  829. {
  830. contentFileBlocks.m_blockOffsetSizePairs = AZStd::vector<BlockOffsetSizePair>{ { 0, inputDataSpan.size() } };
  831. return contentFileBlocks;
  832. }
  833. const Compression::CompressionOptions& compressionOptions = fileSettings.m_compressionOptions != nullptr
  834. ? *fileSettings.m_compressionOptions
  835. : Compression::CompressionOptions{};
  836. // Due to check earlier validating that the inputDataSpan is not empty,
  837. // the compressedBlockCount will be at least 1 due to rounding up to the nearest block
  838. AZ::u32 compressedBlockCount = GetBlockCountIfCompressed(inputDataSpan.size());
  839. // Resize the compress block buffer to be be a multiple of ArchiveBlockSizeForCompression
  840. // times the block count
  841. AZStd::vector<AZStd::byte> compressBlocksBuffer;
  842. compressBlocksBuffer.resize_no_construct(compressedBlockCount * ArchiveBlockSizeForCompression);
  843. // Make sure there is at least one task that runs to make sure that progress
  844. // with compression is always being made
  845. const AZ::u32 maxCompressTasks = AZStd::max(1U, m_settings.m_maxCompressTasks);
  846. // Stores the compressed size for all blocks without alignment
  847. size_t compressedBlockSizeForAllBlocks{};
  848. // allocated slots for each blocks CompressionResultData
  849. AZStd::vector<Compression::CompressionResultData> compressedBlockResults(maxCompressTasks);
  850. while (compressedBlockCount > 0)
  851. {
  852. const AZ::u32 compressionThresholdInBytes = m_archiveHeader.m_compressionThreshold;
  853. const AZ::u32 iterationTaskCount = AZStd::min(compressedBlockCount, maxCompressTasks);
  854. // decrease the compressedBlockCount by the number of compressed tasks to be executed
  855. compressedBlockCount -= iterationTaskCount;
  856. {
  857. // Task graph event used to block when writing compressed blocks in parallel
  858. auto taskWriteGraphEvent = AZStd::make_unique<AZ::TaskGraphEvent>("Content File Compress Sync");
  859. AZ::TaskGraph taskGraph{ "Archive Compress Tasks" };
  860. AZ::TaskDescriptor compressTaskDescriptor{"Compress Block", "Archive Content File Compression"};
  861. for (size_t compressedTaskSlot = 0; compressedTaskSlot < iterationTaskCount; ++compressedTaskSlot)
  862. {
  863. AZStd::span compressBlocksSpan(compressBlocksBuffer);
  864. const size_t blockStartOffset = compressedTaskSlot * ArchiveBlockSizeForCompression;
  865. // Cap the input block span size to the minimum of the ArchiveBlockSizeForCompression(2 MiB) and the remaining size
  866. // left in the input buffer via subspan
  867. const size_t inputBlockSize = AZStd::min(
  868. inputDataSpan.size() - blockStartOffset,
  869. static_cast<size_t>(ArchiveBlockSizeForCompression));
  870. auto inputBlockSpan = inputDataSpan.subspan(blockStartOffset,
  871. inputBlockSize);
  872. // Span that is segmented in up to ArchiveBlockSize(2MiB) blocks to store compressed data
  873. const size_t compressBlockSize = AZStd::min(
  874. compressBlocksSpan.size() - blockStartOffset,
  875. static_cast<size_t>(ArchiveBlockSizeForCompression));
  876. auto compressBlockSpan = compressBlocksSpan.subspan(blockStartOffset,
  877. compressBlockSize);
  878. //! Compress Task to execute in task executor
  879. auto compressTask = [
  880. compressionInterface, &compressionOptions, inputBlockSpan, compressBlockSpan,
  881. &compressedBlockResult = compressedBlockResults[compressedTaskSlot]]()
  882. {
  883. // Run the input data through the compressor
  884. compressedBlockResult = compressionInterface->CompressBlock(
  885. compressBlockSpan, inputBlockSpan, compressionOptions);
  886. };
  887. taskGraph.AddTask(compressTaskDescriptor, AZStd::move(compressTask));
  888. }
  889. taskGraph.SubmitOnExecutor(m_taskWriteExecutor, taskWriteGraphEvent.get());
  890. // Sync on the task completion
  891. taskWriteGraphEvent->Wait();
  892. }
  893. size_t alignedCompressedBlockSizeForAllBlocks{};
  894. for (size_t compressedTaskSlot = 0; compressedTaskSlot < iterationTaskCount; ++compressedTaskSlot)
  895. {
  896. Compression::CompressionResultData& compressedBlockResult = compressedBlockResults[compressedTaskSlot];
  897. if (!compressedBlockResult || compressedBlockResult.GetCompressedByteCount() > compressionThresholdInBytes)
  898. {
  899. // If compression fails for a block or it is higher than the compression threshold
  900. // then return the contentFileData that has the compression algorithm set to uncompressed
  901. // and points to the input data span
  902. // The entire file is stored uncompressed in this scenario
  903. // Return a successful outcome with a single block offset size pair that references the entire
  904. // input buffer
  905. contentFileBlocks.m_blockOffsetSizePairs.assign({ BlockOffsetSizePair{0, inputDataSpan.size()} });
  906. contentFileBlocks.m_totalUnalignedSize = inputDataSpan.size();
  907. return contentFileBlocks;
  908. }
  909. else
  910. {
  911. AZ::u64 compressedBlockSize = compressedBlockResult.GetCompressedByteCount();
  912. compressedBlockSizeForAllBlocks += compressedBlockSize;
  913. alignedCompressedBlockSizeForAllBlocks += AZ_SIZE_ALIGN_UP(compressedBlockSize, ArchiveDefaultBlockAlignment);
  914. }
  915. }
  916. // Determine the amount of additional bytes to resize the compression data output buffer
  917. // This takes into account the alignment of blocks as how it would be written on disk
  918. compressionDataBuffer.reserve(compressionDataBuffer.size() + alignedCompressedBlockSizeForAllBlocks);
  919. for (size_t compressedTaskSlot = 0; compressedTaskSlot < iterationTaskCount; ++compressedTaskSlot)
  920. {
  921. const Compression::CompressionResultData& compressedBlockResult = compressedBlockResults[compressedTaskSlot];
  922. // Calculated the number of additional bytes to store to pad the block to 512-byte alignment
  923. const AZ::u64 alignmentBytes = AZ_SIZE_ALIGN_UP(compressedBlockResult.GetCompressedByteCount(), ArchiveDefaultBlockAlignment)
  924. - compressedBlockResult.GetCompressedByteCount();
  925. // Copy the bytes from block into the data buffer
  926. auto insertIt = compressionDataBuffer.insert(compressionDataBuffer.end(), compressedBlockResult.m_compressedBuffer.begin(),
  927. compressedBlockResult.m_compressedBuffer.end());
  928. auto compressedBlockStartOffset = size_t(AZStd::distance(compressionDataBuffer.begin(), insertIt));
  929. // fill the buffer with padding bytes
  930. compressionDataBuffer.insert(compressionDataBuffer.end(), alignmentBytes, AZStd::byte{});
  931. // Populate the block offset pairs with the offset within compressionDataBuffer where the compressed block is written
  932. // plus the size of the compressed data
  933. contentFileBlocks.m_blockOffsetSizePairs.push_back({ compressedBlockStartOffset,
  934. compressedBlockResult.GetCompressedByteCount() });
  935. }
  936. }
  937. // Set the compression algorithm index once compression has completed successfully for all blocks of the file
  938. contentFileBlocks.m_compressionAlgorithmIndex = static_cast<AZ::u8>(compressionAlgorithmIndex);
  939. // The file has been successfully compressed, so store a span to the buffer
  940. contentFileBlocks.m_writeSpan = compressionDataBuffer;
  941. // Store the compressed size of each block without taking any alignment into account
  942. // This is the exact total compressed size of the "file" as stored in blocks
  943. contentFileBlocks.m_totalUnalignedSize = compressedBlockSizeForAllBlocks;
  944. return contentFileBlocks;
  945. }
  946. ArchiveFileToken ArchiveWriter::WriteContentFileToArchive(const ArchiveWriterFileSettings& fileSettings,
  947. const ContentFileData& contentFileData)
  948. {
  949. if (fileSettings.m_fileMode == ArchiveWriterFileMode::AddNew)
  950. {
  951. // Update the file count in the archive
  952. ++m_archiveHeader.m_fileCount;
  953. }
  954. // Locate the location within the Archive to write the file data
  955. // First any deleted blocks are located to see if the file data can be written to it
  956. // otherwise the content data is written at the current table of contents offset
  957. // and the table of contents offset is then shifted by that amount
  958. // The m_relativeFilePath is guaranteed to not be empty due to the check at the top of AddFileToArchive
  959. // If the file path already exist in the archive locate it
  960. auto findArchiveTokenIt = m_pathMap.find(contentFileData.m_relativeFilePath);
  961. // Insert the file path to the end of the file path index table if the file path is not in the archive
  962. size_t archiveFileIndex{};
  963. if (findArchiveTokenIt != m_pathMap.end())
  964. {
  965. // If the file exist in the archive, store its index
  966. archiveFileIndex = findArchiveTokenIt->second;
  967. }
  968. else if (!m_removedFileIndices.empty())
  969. {
  970. // If the removedFileIndices set is not empty, store that index
  971. auto firstDeletedFileIt = m_removedFileIndices.begin();
  972. archiveFileIndex = *firstDeletedFileIt;
  973. // Pop off the index of the from the removed file indices sets
  974. m_removedFileIndices.erase(firstDeletedFileIt);
  975. }
  976. else
  977. {
  978. // In this case, the file path does not exist as part of the existing archive
  979. // and the removed file indices set is empty
  980. // Get the current size of the file path index table
  981. archiveFileIndex = m_archiveToc.m_fileMetadataTable.size();
  982. // Append a new entry to each of the Archive TOC file metadata containers
  983. m_archiveToc.m_fileMetadataTable.emplace_back();
  984. m_archiveToc.m_filePaths.emplace_back();
  985. }
  986. // Get reference to the FileMetadata entry in the Archive
  987. ArchiveTocFileMetadata& fileMetadata = m_archiveToc.m_fileMetadataTable[archiveFileIndex];
  988. fileMetadata.m_uncompressedSize = contentFileData.m_uncompressedSpan.size();
  989. // Divide by the ArchiveDefaultBlockAlignment(512) to convert the compressedSize to sectors
  990. const AZ::u64 alignedFileSize = AZ_SIZE_ALIGN_UP(contentFileData.m_contentFileBlocks.m_writeSpan.size(), ArchiveDefaultBlockAlignment);
  991. fileMetadata.m_compressedSizeInSectors = alignedFileSize / ArchiveDefaultBlockAlignment;
  992. fileMetadata.m_compressionAlgoIndex = contentFileData.m_contentFileBlocks.m_compressionAlgorithmIndex;
  993. fileMetadata.m_offset = ExtractWriteBlockOffset(alignedFileSize);
  994. fileMetadata.m_crc32 = AZ::Crc32(contentFileData.m_uncompressedSpan);
  995. ArchiveTableOfContents::Path& filePath = m_archiveToc.m_filePaths[archiveFileIndex];
  996. filePath = contentFileData.m_relativeFilePath;
  997. {
  998. AZStd::span<const AZStd::byte> contiguousWriteSpan = contentFileData.m_contentFileBlocks.m_writeSpan;
  999. // Write out the blocks to the stream
  1000. AZStd::scoped_lock writeLock(m_archiveStreamMutex);
  1001. m_archiveStream->Seek(fileMetadata.m_offset, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  1002. m_archiveStream->Write(contiguousWriteSpan.size(), contiguousWriteSpan.data());
  1003. }
  1004. // Update the block offset table if the file is compressed
  1005. if (contentFileData.m_contentFileBlocks.m_compressionAlgorithmIndex < UncompressedAlgorithmIndex)
  1006. {
  1007. fileMetadata.m_blockLineTableFirstIndex = UpdateBlockOffsetEntryForFile(contentFileData);
  1008. }
  1009. m_pathMap[filePath] = archiveFileIndex;
  1010. return static_cast<ArchiveFileToken>(archiveFileIndex);
  1011. }
  1012. AZ::u64 ArchiveWriter::UpdateBlockOffsetEntryForFile(const ContentFileData& contentFileData)
  1013. {
  1014. // Index into the block offset table first entry for the file
  1015. AZ::u64 blockLineFirstIndex = m_archiveToc.m_blockOffsetTable.size();
  1016. // Reserve space for the number of block line entries stored
  1017. AZ::u64 remainingUncompressedSize = contentFileData.m_uncompressedSpan.size();
  1018. size_t blockLineCount = GetBlockLineCountIfCompressed(remainingUncompressedSize);
  1019. m_archiveToc.m_blockOffsetTable.reserve(m_archiveToc.m_blockOffsetTable.size() + blockLineCount);
  1020. size_t blockOffsetIndex{};
  1021. // Three block lines which is up to 18MiB of uncompressed data is handled
  1022. // each iteration of the loop.
  1023. // If the remaining uncompressed size is <=18MiB then the last iteration of the loop
  1024. // handles the remaining block lines for which there can be 1(<= 6 MiB) to 3 (> 12 MiB && <= 18 MiB)
  1025. while (blockLineCount > 0 && blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1026. {
  1027. if (remainingUncompressedSize > MaxRemainingFileSizeNoJumpEntry)
  1028. {
  1029. // Stores the jump offset which can be used to skip 16 MiB of uncompressed content
  1030. // This is calculated by summing the 512-bit aligned compressed sizes of each block
  1031. AZ::u16 jumpOffset{};
  1032. size_t blockLineWithJumpIndex = AZStd::numeric_limits<size_t>::max();
  1033. BlockOffsetSizePair blockOffsetSizePair;
  1034. // Stores sizes for compressed data from offsets (0-4MiB]
  1035. {
  1036. ArchiveBlockLineUnion& blockLine1 = m_archiveToc.m_blockOffsetTable.emplace_back();
  1037. // Tracks the index of the block line vector element with a jump entry
  1038. // The jump entry offset needs to be updated after the next 8 block line aligned compressed sizes
  1039. // have been calculated
  1040. // NOTE: The `blockLine1` is not safe to use later on, as further emplace_back calls could invalidate
  1041. // the reference via reallocation, so index into the vector is being used
  1042. blockLineWithJumpIndex = m_archiveToc.m_blockOffsetTable.size();
  1043. // Set the first block as used
  1044. blockLine1.m_blockLineWithJump.m_blockUsed = 1;
  1045. // Write out the first block offset entry
  1046. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1047. {
  1048. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1049. blockLine1.m_blockLineWithJump.m_block0 = blockOffsetSizePair.m_size;
  1050. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1051. / ArchiveDefaultBlockAlignment);
  1052. }
  1053. // Write out the second block offset entry
  1054. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1055. {
  1056. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1057. blockLine1.m_blockLineWithJump.m_block1 = blockOffsetSizePair.m_size;
  1058. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1059. / ArchiveDefaultBlockAlignment);
  1060. }
  1061. }
  1062. // The second block line represents the offsets of (4MiB-10MiB]
  1063. {
  1064. ArchiveBlockLineUnion& blockLine2 = m_archiveToc.m_blockOffsetTable.emplace_back();
  1065. // Set the second block as used
  1066. blockLine2.m_blockLine.m_blockUsed = 1;
  1067. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1068. {
  1069. // Write out the third block offset entry
  1070. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1071. blockLine2.m_blockLine.m_block0 = blockOffsetSizePair.m_size;
  1072. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1073. / ArchiveDefaultBlockAlignment);
  1074. }
  1075. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1076. {
  1077. // Write out the fourth block offset entry
  1078. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1079. blockLine2.m_blockLine.m_block1 = blockOffsetSizePair.m_size;
  1080. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1081. / ArchiveDefaultBlockAlignment);
  1082. }
  1083. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1084. {
  1085. // Write out the fifth block offset entry
  1086. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1087. blockLine2.m_blockLine.m_block2 = blockOffsetSizePair.m_size;
  1088. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1089. / ArchiveDefaultBlockAlignment);
  1090. }
  1091. }
  1092. // The third block line represents the offsets of (10MiB-16MiB]
  1093. {
  1094. ArchiveBlockLineUnion& blockLine3 = m_archiveToc.m_blockOffsetTable.emplace_back();
  1095. // Set the third block as used
  1096. blockLine3.m_blockLine.m_blockUsed = 1;
  1097. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1098. {
  1099. // Write out the sixth block offset entry
  1100. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1101. blockLine3.m_blockLine.m_block0 = blockOffsetSizePair.m_size;
  1102. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1103. / ArchiveDefaultBlockAlignment);
  1104. }
  1105. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1106. {
  1107. // Write out the seventh block offset entry
  1108. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1109. blockLine3.m_blockLine.m_block1 = blockOffsetSizePair.m_size;
  1110. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1111. / ArchiveDefaultBlockAlignment);
  1112. }
  1113. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1114. {
  1115. // Write out the eighth block offset entry
  1116. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1117. blockLine3.m_blockLine.m_block2 = blockOffsetSizePair.m_size;
  1118. jumpOffset += aznumeric_cast<AZ::u16>(AZ_SIZE_ALIGN_UP(blockOffsetSizePair.m_size, ArchiveDefaultBlockAlignment)
  1119. / ArchiveDefaultBlockAlignment);
  1120. }
  1121. }
  1122. // Now update the jump offset value with the amount of bytes that can be skipped in the C data section of the archive
  1123. // from the beginning of the file
  1124. m_archiveToc.m_blockOffsetTable[blockLineWithJumpIndex].m_blockLineWithJump.m_blockJump = jumpOffset;
  1125. // The first block offset entry is a jump table entry
  1126. // while the next 8 block offset entries store compressed sizes
  1127. // for 2 MiB chunks which total 16 MiB
  1128. // The three block lines are represented by 3 G4-bit integers
  1129. //
  1130. // 64-bit block line #1 (57-bits used)
  1131. // Jump Entry : 16-bits
  1132. // Block #0 : 21-bits
  1133. // Block #1 : 21-bits
  1134. // 64-bit block line #2 (63-bits used)
  1135. // Block #2 : 21-bits
  1136. // Block #3 : 21-bits
  1137. // Block #4 : 21-bits
  1138. // 64-bit block line #3 (63-bits used)
  1139. // Block #5 : 21-bits
  1140. // Block #6 : 21-bits
  1141. // Block #7 : 21-bits
  1142. remainingUncompressedSize -= FileSizeToSkipWithJumpEntry;
  1143. // As 3 blocks are being processed at a time decrement the block line count by 3
  1144. blockLineCount = blockLineCount > 2 ? blockLineCount - 3 : 0;
  1145. }
  1146. else
  1147. {
  1148. BlockOffsetSizePair blockOffsetSizePair;
  1149. // Store each 6 MiB block line
  1150. for (; remainingUncompressedSize > 0; remainingUncompressedSize -= AZStd::min(remainingUncompressedSize, MaxBlockLineSize))
  1151. {
  1152. ArchiveBlockLineUnion& blockLine1 = m_archiveToc.m_blockOffsetTable.emplace_back();
  1153. // Set the first block as used
  1154. blockLine1.m_blockLine.m_blockUsed = 1;
  1155. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1156. {
  1157. // Write out the first block offset entry
  1158. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1159. blockLine1.m_blockLine.m_block0 = blockOffsetSizePair.m_size;
  1160. }
  1161. // Write out the second block offset entry
  1162. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1163. {
  1164. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1165. blockLine1.m_blockLine.m_block1 = blockOffsetSizePair.m_size;
  1166. }
  1167. // Write out the third block offset entry
  1168. if (blockOffsetIndex < contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs.size())
  1169. {
  1170. blockOffsetSizePair = contentFileData.m_contentFileBlocks.m_blockOffsetSizePairs[blockOffsetIndex++];
  1171. blockLine1.m_blockLine.m_block2 = blockOffsetSizePair.m_size;
  1172. }
  1173. }
  1174. // The remaining uncompressed size <= 18 MiB, so a block jump entry is not used
  1175. // Up to the next 9 blocks(3 block lines) if needed will encode the 2 MiB chunks
  1176. // which can total up to 18 MiB
  1177. //
  1178. // 64-bit block line #1 (63-bits used)
  1179. // Block #0 : 21-bits
  1180. // Block #1 : 21-bits
  1181. // Block #2 : 21-bits
  1182. // 64-bit block line #2 (63-bits used)
  1183. // Block #3 : 21-bits
  1184. // Block #4 : 21-bits
  1185. // Block #5 : 21-bits
  1186. // 64-bit block line #3 (63-bits used)
  1187. // Block #6 : 21-bits
  1188. // Block #7 : 21-bits
  1189. // Block #8 : 21-bits
  1190. remainingUncompressedSize = 0;
  1191. // There are no more block lines to process after this loop
  1192. blockLineCount = 0;
  1193. }
  1194. }
  1195. return blockLineFirstIndex;
  1196. }
  1197. AZ::u64 ArchiveWriter::ExtractWriteBlockOffset(AZ::u64 alignedFileSizeToWrite)
  1198. {
  1199. // If the file size is 0, then the offset value doesn't matter
  1200. // return 0 in this case
  1201. if (alignedFileSizeToWrite == 0)
  1202. {
  1203. return 0ULL;
  1204. }
  1205. if (auto deletedBlockIt = m_deletedBlockSizeToOffsetMap.lower_bound(alignedFileSizeToWrite);
  1206. deletedBlockIt != m_deletedBlockSizeToOffsetMap.end())
  1207. {
  1208. // Get the full size of the deleted block
  1209. const AZ::u64 deletedBlockSize = deletedBlockIt->first;
  1210. // gets reference to AZStd::set of deleted blocks
  1211. if (auto& blockOffsetSet = deletedBlockIt->second; !blockOffsetSet.empty())
  1212. {
  1213. // extract the first element from the deleted block offset set
  1214. // for a specific block size
  1215. auto blockOffsetHandle = blockOffsetSet.extract(blockOffsetSet.begin());
  1216. // If the block offset set is now empty for a specific block size,
  1217. // erase it from the deleted block size offset map
  1218. if (blockOffsetSet.empty())
  1219. {
  1220. m_deletedBlockSizeToOffsetMap.erase(deletedBlockIt);
  1221. }
  1222. // copy the deleted block offset from the extracted value from the set
  1223. const AZ::u64 deletedBlockWriteOffset = blockOffsetHandle.value();
  1224. // insert a smaller deleted block back into the deleted block size offset map
  1225. // if the entire deleted block is not used
  1226. // Since blocks are 512-byte aligned, the newDeletedBlockSize is rounded down to the nearest
  1227. // the 512-byte boundary and checked if it is >0
  1228. if(AZ::u64 newAlignedDeletedBlockSize = deletedBlockSize - alignedFileSizeToWrite;
  1229. newAlignedDeletedBlockSize > 0)
  1230. {
  1231. // Calculate the new deleted block offset by aligning up to the nearest 512-byte boundary
  1232. const AZ::u64 newDeletedBlockOffset = AZ_SIZE_ALIGN_UP(deletedBlockWriteOffset + alignedFileSizeToWrite,
  1233. ArchiveDefaultBlockAlignment);
  1234. auto& deletedBlockSetForSize = m_deletedBlockSizeToOffsetMap[newAlignedDeletedBlockSize];
  1235. deletedBlockSetForSize.emplace(newDeletedBlockOffset);
  1236. }
  1237. return deletedBlockWriteOffset;
  1238. }
  1239. }
  1240. // Fall back to returning the archive header TOC offset as the write block offset
  1241. AZ::u64 writeBlockOffset = m_archiveHeader.m_tocOffset;
  1242. // Move the archive header TOC offset forward by the file size that will be written
  1243. m_archiveHeader.m_tocOffset = writeBlockOffset + alignedFileSizeToWrite;
  1244. return writeBlockOffset;
  1245. }
  1246. ArchiveFileToken ArchiveWriter::FindFile(AZ::IO::PathView relativePath) const
  1247. {
  1248. auto foundIt = m_pathMap.find(relativePath);
  1249. return foundIt != m_pathMap.end() ? static_cast<ArchiveFileToken>(foundIt->second) : InvalidArchiveFileToken;
  1250. }
  1251. bool ArchiveWriter::ContainsFile(AZ::IO::PathView relativePath) const
  1252. {
  1253. return m_pathMap.contains(relativePath);
  1254. }
  1255. ArchiveRemoveFileResult ArchiveWriter::RemoveFileFromArchive(ArchiveFileToken filePathToken)
  1256. {
  1257. ArchiveRemoveFileResult result;
  1258. const size_t archiveFileIndex = static_cast<size_t>(filePathToken);
  1259. if (archiveFileIndex < m_archiveToc.m_fileMetadataTable.size()
  1260. && !m_removedFileIndices.contains(archiveFileIndex))
  1261. {
  1262. // Add the archive file index to the set of removed file indices
  1263. m_removedFileIndices.emplace(archiveFileIndex);
  1264. // Get a reference to the table of contents entry being remove and add its blocks to the deleted block map
  1265. ArchiveTocFileMetadata& fileMetadata = m_archiveToc.m_fileMetadataTable[archiveFileIndex];
  1266. AZ::u64 blockSize = fileMetadata.m_compressionAlgoIndex == UncompressedAlgorithmIndex
  1267. ? fileMetadata.m_uncompressedSize
  1268. : fileMetadata.m_compressedSizeInSectors * ArchiveDefaultBlockAlignment;
  1269. // FYI: The compressed size in sectors is the aggregate represents the total size of the compressed
  1270. // 2-MiB blocks as stored in the raw data
  1271. // See `ArchiveTocFileMetadata` structure for more info
  1272. AZ::u64 alignedBlockSize = AZ_SIZE_ALIGN_UP(blockSize, ArchiveDefaultBlockAlignment);
  1273. AZ::u64 alignedBlockOffset = AZ_SIZE_ALIGN_UP(fileMetadata.m_offset, ArchiveDefaultBlockAlignment);
  1274. // If the new block size aligned down to nearest 512-byte boundary is 0
  1275. // then there deleted blocks
  1276. if (alignedBlockSize > 0)
  1277. {
  1278. auto& deletedBlockOffsetSet = m_deletedBlockSizeToOffsetMap[alignedBlockSize];
  1279. deletedBlockOffsetSet.emplace(alignedBlockOffset);
  1280. }
  1281. // Update the result structure with the metadata about the removed file
  1282. result.m_uncompressedSize = fileMetadata.m_uncompressedSize;
  1283. // Get the actual size that the compressed data takes on disk
  1284. if (auto rawFileSizeResult = GetRawFileSize(fileMetadata, m_archiveToc.m_blockOffsetTable);
  1285. rawFileSizeResult)
  1286. {
  1287. result.m_compressedSize = rawFileSizeResult.value();
  1288. }
  1289. result.m_offset = fileMetadata.m_offset;
  1290. // If the was compressed, retrieve the compression algorithm Id associated with the index
  1291. if (fileMetadata.m_compressionAlgoIndex < UncompressedAlgorithmIndex)
  1292. {
  1293. result.m_compressionAlgorithm = m_archiveHeader.m_compressionAlgorithmsIds[fileMetadata.m_compressionAlgoIndex];
  1294. }
  1295. // Clear out the FileMetadata entry from Accelerating Table of Contents structure
  1296. fileMetadata = {};
  1297. // Move the file path stored in the table of contents into the result structure
  1298. result.m_relativeFilePath = static_cast<AZ::IO::Path&&>(AZStd::move(m_archiveToc.m_filePaths[archiveFileIndex]));
  1299. // Remove the file path -> file token store for this ArchiveWriter
  1300. if (const size_t erasedPathCount = m_pathMap.erase(result.m_relativeFilePath);
  1301. erasedPathCount == 0)
  1302. {
  1303. result.m_resultOutcome = AZStd::unexpected(ResultString::format("Removing mapping of file path from the Archive Writer"
  1304. R"(file path -> archive file token map failed to locate path "%s")", result.m_relativeFilePath.c_str()));
  1305. }
  1306. // Decrement the file count in the header
  1307. --m_archiveHeader.m_fileCount;
  1308. }
  1309. return result;
  1310. }
  1311. ArchiveRemoveFileResult ArchiveWriter::RemoveFileFromArchive(AZ::IO::PathView relativePath)
  1312. {
  1313. const auto pathIt = m_pathMap.find(relativePath);
  1314. return pathIt != m_pathMap.end() ? RemoveFileFromArchive(static_cast<ArchiveFileToken>(pathIt->second))
  1315. : ArchiveRemoveFileResult{};
  1316. }
  1317. bool ArchiveWriter::DumpArchiveMetadata(AZ::IO::GenericStream& metadataStream,
  1318. const ArchiveMetadataSettings& metadataSettings) const
  1319. {
  1320. using MetadataString = AZStd::fixed_string<256>;
  1321. if (metadataSettings.m_writeFileCount)
  1322. {
  1323. auto fileCountString = MetadataString::format("Total File Count: %u\n", m_archiveHeader.m_fileCount);
  1324. metadataStream.Write(fileCountString.size(), fileCountString.data());
  1325. }
  1326. if (metadataSettings.m_writeFilePaths)
  1327. {
  1328. // Validate the file path and file metadata tables are in sync
  1329. if (m_archiveToc.m_filePaths.size() != m_archiveToc.m_fileMetadataTable.size())
  1330. {
  1331. auto errorString = MetadataString::format("Error: The Archive TOC of contents has a mismatched size between"
  1332. " the file path vector (size=%zu) and the file metadata vector (size=%zu).\n"
  1333. "This indicates a code error in the ArchiveWriter.",
  1334. m_archiveToc.m_filePaths.size(), m_archiveToc.m_fileMetadataTable.size());
  1335. metadataStream.Write(errorString.size(), errorString.data());
  1336. return false;
  1337. }
  1338. // Tracks the offset of current non-deleted file entry in the table of contents
  1339. // Deleted entries in the table of contents are skipped
  1340. size_t activeFileOffset{};
  1341. for (size_t fileMetadataTableIndex{}; fileMetadataTableIndex < m_archiveToc.m_filePaths.size(); ++fileMetadataTableIndex)
  1342. {
  1343. const ArchiveTableOfContents::Path& contentFilePath = m_archiveToc.m_filePaths[fileMetadataTableIndex];
  1344. // An empty file path is used to track removed files from the archive,
  1345. // therefore only non-empty paths are iterated
  1346. if (!contentFilePath.empty())
  1347. {
  1348. const ArchiveTocFileMetadata& contentFileMetadata = m_archiveToc.m_fileMetadataTable[fileMetadataTableIndex];
  1349. auto fileMetadataString = MetadataString::format(R"(File %zu: path="%s")", activeFileOffset, contentFilePath.c_str());
  1350. if (metadataSettings.m_writeFileOffsets)
  1351. {
  1352. fileMetadataString += MetadataString::format(R"(, offset=%llu)", contentFileMetadata.m_offset);
  1353. }
  1354. if (metadataSettings.m_writeFileSizesAndCompression)
  1355. {
  1356. fileMetadataString += MetadataString::format(R"(, uncompressed_size=%llu)", contentFileMetadata.m_uncompressedSize);
  1357. // Only output compressed size if an compression that compresses data is being used
  1358. if (contentFileMetadata.m_compressionAlgoIndex < UncompressedAlgorithmIndex)
  1359. {
  1360. if (auto rawFileSizeResult = GetRawFileSize(contentFileMetadata, m_archiveToc.m_blockOffsetTable);
  1361. rawFileSizeResult)
  1362. {
  1363. AZ::u64 compressedSize = rawFileSizeResult.value();
  1364. fileMetadataString += MetadataString::format(R"(, compressed_size=%llu)",
  1365. compressedSize);
  1366. }
  1367. fileMetadataString += MetadataString::format(R"(, compression_algorithm_id=%u)",
  1368. AZStd::to_underlying(m_archiveHeader.m_compressionAlgorithmsIds[contentFileMetadata.m_compressionAlgoIndex]));
  1369. }
  1370. }
  1371. // Append a newline before writing to the stream
  1372. fileMetadataString.push_back('\n');
  1373. metadataStream.Write(fileMetadataString.size(), fileMetadataString.data());
  1374. // Increment the active file offset for non-removed files
  1375. ++activeFileOffset;
  1376. }
  1377. }
  1378. }
  1379. return true;
  1380. }
  1381. } // namespace Archive