ArchiveWriter.cpp 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565
  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