ArchiveWriterTest.cpp 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  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 <AzCore/UnitTest/TestTypes.h>
  9. #include <AzCore/IO/ByteContainerStream.h>
  10. #include <AzCore/std/ranges/ranges_algorithm.h>
  11. #include <Archive/Tools/ArchiveWriterAPI.h>
  12. #include <Compression/CompressionLZ4API.h>
  13. // Archive Gem private implementation includes
  14. #include <Tools/ArchiveWriterFactory.h>
  15. namespace Archive::Test
  16. {
  17. // The ArchiveEditorTestEnvironment is tracking memory
  18. // via the GemTestEnvironment::SetupEnvironment function
  19. // so the LeakDetectionFixture should not be used
  20. class ArchiveWriterFixture
  21. : public ::testing::Test
  22. {
  23. public:
  24. ArchiveWriterFixture()
  25. {
  26. m_archiveWriterFactory = AZStd::make_unique<ArchiveWriterFactory>();
  27. AZ::Interface<IArchiveWriterFactory>::Register(m_archiveWriterFactory.get());
  28. }
  29. ~ArchiveWriterFixture()
  30. {
  31. AZ::Interface<IArchiveWriterFactory>::Unregister(m_archiveWriterFactory.get());
  32. }
  33. protected:
  34. // Create and register Archive Writer factory
  35. // to allow IArchiveWriter instances to be created
  36. AZStd::unique_ptr<IArchiveWriterFactory> m_archiveWriterFactory;
  37. };
  38. // Helper function for converting a string view to a byte span
  39. // This performs a reinterpret_cast on the string_view as buffer as pointer
  40. // to a contiguous sequence of AZStd::byte objects
  41. auto StringToByteSpan(AZStd::string_view textData) -> AZStd::span<const AZStd::byte>
  42. {
  43. return AZStd::as_bytes(AZStd::span(textData));
  44. }
  45. TEST_F(ArchiveWriterFixture, CreateArchiveWriter_Succeeds)
  46. {
  47. {
  48. auto createArchiveWriterResult = CreateArchiveWriter();
  49. ASSERT_TRUE(createArchiveWriterResult);
  50. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  51. ASSERT_NE(nullptr, archiveWriter);
  52. }
  53. {
  54. auto createArchiveWriterResult = CreateArchiveWriter(ArchiveWriterSettings{});
  55. ASSERT_TRUE(createArchiveWriterResult);
  56. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  57. ASSERT_NE(nullptr, archiveWriter);
  58. }
  59. }
  60. TEST_F(ArchiveWriterFixture, EmptyArchive_Create_Succeeds)
  61. {
  62. AZStd::vector<AZStd::byte> archiveBuffer;
  63. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  64. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  65. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  66. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  67. ASSERT_TRUE(createArchiveWriterResult);
  68. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  69. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  70. ASSERT_TRUE(commitResult);
  71. // The empty archive should be have a header equal to the default constructed ArchiveHeader
  72. ArchiveHeader defaultArchiveHeader;
  73. auto defaultArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&defaultArchiveHeader), sizeof(defaultArchiveHeader));
  74. // The ArchiveHeader is written out as a 512-byte aligned
  75. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  76. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, defaultArchiveHeaderSpan));
  77. }
  78. TEST_F(ArchiveWriterFixture, ExistingArchive_CanBeWritten_Succeeds)
  79. {
  80. AZStd::vector<AZStd::byte> archiveBuffer;
  81. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  82. // Write an empty archive
  83. {
  84. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  85. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  86. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  87. ASSERT_TRUE(createArchiveWriterResult);
  88. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  89. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  90. ASSERT_TRUE(commitResult);
  91. // The empty archive should be have a header equal to the default constructed ArchiveHeader
  92. ArchiveHeader defaultArchiveHeader;
  93. auto defaultArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&defaultArchiveHeader), sizeof(defaultArchiveHeader));
  94. // The ArchiveHeader is written out as a 512-byte aligned
  95. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  96. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, defaultArchiveHeaderSpan));
  97. }
  98. // Seek back to the beginning of the archiveStream and re-use it
  99. archiveStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  100. {
  101. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  102. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  103. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  104. ASSERT_TRUE(createArchiveWriterResult);
  105. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  106. // Recommit the existing archive with no changes
  107. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  108. ASSERT_TRUE(commitResult);
  109. ArchiveHeader defaultArchiveHeader;
  110. auto defaultArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&defaultArchiveHeader), sizeof(defaultArchiveHeader));
  111. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  112. // As no changes have been made to the existing archive empty archive it should still compare equal to a
  113. // default constructed archive header
  114. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, defaultArchiveHeaderSpan));
  115. }
  116. }
  117. TEST_F(ArchiveWriterFixture, Archive_WithSingleUncompressedFileAdded_Succeeds)
  118. {
  119. AZStd::vector<AZStd::byte> archiveBuffer;
  120. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  121. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  122. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  123. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  124. ASSERT_TRUE(createArchiveWriterResult);
  125. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  126. // Add an uncompressed file to the Archive
  127. AZStd::string_view fileContent = "Hello World";
  128. ArchiveWriterFileSettings fileSettings;
  129. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  130. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  131. EXPECT_TRUE(addFileResult);
  132. // A successfully added file should not return InvalidArchiveFileToken
  133. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  134. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  135. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  136. AZStd::to_lower(loweredCaseFilePath.Native());
  137. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  138. // The file should not be compressed
  139. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  140. // Commit the archive Header and Table of Contents to the stream
  141. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  142. ASSERT_TRUE(commitResult);
  143. ArchiveHeader expectedArchiveHeader;
  144. // As there is one file in the archive which is less than 512 bytes
  145. // The expected Table of Contents offset would be
  146. // 512 to account for the table of contents + 512 for the single file block that is aligned to the
  147. // next multiple of 512
  148. // So the Table of Contents should start at offset 1024
  149. expectedArchiveHeader.m_tocOffset = ArchiveDefaultBlockAlignment + ArchiveDefaultBlockAlignment;
  150. expectedArchiveHeader.m_fileCount = 1;
  151. // Update the uncompressed sizes for the table of contents based on the number of files in the toc
  152. // plus the number of block lines used by that file
  153. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 1;
  154. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 1;
  155. // The path blob should only contain the single filename
  156. expectedArchiveHeader.m_tocPathBlobUncompressedSize = static_cast<AZ::u32>(fileSettings.m_relativeFilePath.Native().size());
  157. // As the file is not compressed the block offset table should not have an entry in it
  158. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = 0;
  159. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  160. // The ArchiveHeader is written out as a 512-byte aligned
  161. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  162. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  163. }
  164. TEST_F(ArchiveWriterFixture, Archive_WithSingleLZ4CompressedFileAdded_Succeeds)
  165. {
  166. AZStd::vector<AZStd::byte> archiveBuffer;
  167. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  168. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  169. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  170. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  171. ASSERT_TRUE(createArchiveWriterResult);
  172. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  173. // Add an uncompressed file to the Archive
  174. AZStd::string_view fileContent = "Hello World";
  175. ArchiveWriterFileSettings fileSettings;
  176. // For this test also validate the upper casing of an added file
  177. fileSettings.m_fileCase = ArchiveFilePathCase::Uppercase;
  178. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  179. fileSettings.m_compressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  180. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  181. EXPECT_TRUE(addFileResult);
  182. // A successfully added file should not return InvalidArchiveFileToken
  183. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  184. // the file settings indicate that the relative path should be added as uppercased
  185. AZ::IO::Path upperCaseFilePath = fileSettings.m_relativeFilePath;
  186. AZStd::to_upper(upperCaseFilePath.Native());
  187. EXPECT_EQ(upperCaseFilePath, addFileResult.m_relativeFilePath);
  188. // The file should be compressed using the LZ4 compression algorithm
  189. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), addFileResult.m_compressionAlgorithm);
  190. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  191. ASSERT_TRUE(commitResult);
  192. ArchiveHeader expectedArchiveHeader;
  193. // As there is one file in the archive which is less than 512 bytes
  194. // The expected Table of Contents offset would be
  195. // 512 to account for the table of contents + 512 for the single file block that is aligned to the
  196. // next multiple of 512
  197. // So the Table of Contents should start at offset 1024
  198. expectedArchiveHeader.m_tocOffset = ArchiveDefaultBlockAlignment + ArchiveDefaultBlockAlignment;
  199. expectedArchiveHeader.m_fileCount = 1;
  200. // Update the uncompressed sizes for the table of contents based on the number of files in the toc
  201. // plus the number of block lines used by that file
  202. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 1;
  203. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 1;
  204. // The path blob should only contain the single filename
  205. expectedArchiveHeader.m_tocPathBlobUncompressedSize = static_cast<AZ::u32>(fileSettings.m_relativeFilePath.Native().size());
  206. // The file is compressed in this case, so there should be single block line entry as the file uncompressed size
  207. // is below Archive::MaxBlockLineSize which is made up of 3 * 2 MiB blocks encoded in a single AZ::u64
  208. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = sizeof(ArchiveBlockLineUnion) * 1;
  209. // Since a compression algorithm is being used set the first entry in the ArchiveHeader m_compressionAlgorithmIds array
  210. size_t currentCompressionAlgorithmIndex{};
  211. expectedArchiveHeader.m_compressionAlgorithmsIds[currentCompressionAlgorithmIndex++] = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  212. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  213. // The ArchiveHeader is written out as a 512-byte aligned
  214. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  215. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  216. }
  217. TEST_F(ArchiveWriterFixture, FindFile_OnlyReturnsFilesInsideArchive)
  218. {
  219. AZStd::vector<AZStd::byte> archiveBuffer;
  220. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  221. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  222. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  223. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  224. ASSERT_TRUE(createArchiveWriterResult);
  225. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  226. AZStd::array<ArchiveAddFileResult, 2> addedFileResults;
  227. {
  228. // Add first file to archive
  229. AZStd::string_view fileContent = "Hello World";
  230. ArchiveWriterFileSettings fileSettings;
  231. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  232. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  233. EXPECT_TRUE(addFileResult);
  234. // A successfully added file should not return InvalidArchiveFileToken
  235. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  236. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  237. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  238. AZStd::to_lower(loweredCaseFilePath.Native());
  239. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  240. // The file should not be compressed
  241. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  242. addedFileResults[0] = AZStd::move(addFileResult);
  243. }
  244. {
  245. // Add second file to archive
  246. AZStd::string_view fileContent = "Goodbye World";
  247. ArchiveWriterFileSettings fileSettings;
  248. fileSettings.m_relativeFilePath = "Sanity/chess.txt";
  249. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  250. EXPECT_TRUE(addFileResult);
  251. // A successfully added file should not return InvalidArchiveFileToken
  252. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  253. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  254. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  255. AZStd::to_lower(loweredCaseFilePath.Native());
  256. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  257. // The file should not be compressed
  258. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  259. addedFileResults[1] = AZStd::move(addFileResult);
  260. }
  261. // Query the file using the path
  262. EXPECT_EQ(addedFileResults[0].m_filePathToken, archiveWriter->FindFile(addedFileResults[0].m_relativeFilePath));
  263. EXPECT_TRUE(archiveWriter->ContainsFile(addedFileResults[0].m_relativeFilePath));
  264. EXPECT_EQ(addedFileResults[1].m_filePathToken, archiveWriter->FindFile(addedFileResults[1].m_relativeFilePath));
  265. EXPECT_TRUE(archiveWriter->ContainsFile(addedFileResults[1].m_relativeFilePath));
  266. // Validate an empty path is not found
  267. EXPECT_EQ(InvalidArchiveFileToken, archiveWriter->FindFile(""));
  268. EXPECT_FALSE(archiveWriter->ContainsFile(""));
  269. // Validate a path not in the archive is not found
  270. constexpr AZ::IO::PathView notInArchivePath = "NotInArchive";
  271. EXPECT_EQ(InvalidArchiveFileToken, archiveWriter->FindFile(notInArchivePath));
  272. EXPECT_FALSE(archiveWriter->ContainsFile(notInArchivePath));
  273. // Commit the archive Header and Table of Contents to the stream
  274. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  275. ASSERT_TRUE(commitResult);
  276. // Second check - Make sure commiting the table of contents to the Archive Stream
  277. // does affect the ability to search files within the archive
  278. EXPECT_EQ(addedFileResults[0].m_filePathToken, archiveWriter->FindFile(addedFileResults[0].m_relativeFilePath));
  279. EXPECT_TRUE(archiveWriter->ContainsFile(addedFileResults[0].m_relativeFilePath));
  280. EXPECT_EQ(addedFileResults[1].m_filePathToken, archiveWriter->FindFile(addedFileResults[1].m_relativeFilePath));
  281. EXPECT_TRUE(archiveWriter->ContainsFile(addedFileResults[1].m_relativeFilePath));
  282. // empty path
  283. EXPECT_EQ(InvalidArchiveFileToken, archiveWriter->FindFile(""));
  284. EXPECT_FALSE(archiveWriter->ContainsFile(""));
  285. // not in archive path
  286. EXPECT_EQ(InvalidArchiveFileToken, archiveWriter->FindFile(notInArchivePath));
  287. EXPECT_FALSE(archiveWriter->ContainsFile(notInArchivePath));
  288. }
  289. TEST_F(ArchiveWriterFixture, Archive_MountAndUnmount_Succeeds)
  290. {
  291. AZStd::vector<AZStd::byte> archiveBuffer;
  292. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  293. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  294. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  295. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  296. ASSERT_TRUE(createArchiveWriterResult);
  297. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  298. EXPECT_TRUE(archiveWriter->IsMounted());
  299. ArchiveHeader expectedArchiveHeader;
  300. {
  301. // Write a uncompressed file to the mounted archive
  302. AZStd::string_view fileContent = "Hello World";
  303. ArchiveWriterFileSettings fileSettings;
  304. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  305. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  306. EXPECT_TRUE(addFileResult);
  307. // A successfully added file should not return InvalidArchiveFileToken
  308. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  309. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  310. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  311. AZStd::to_lower(loweredCaseFilePath.Native());
  312. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  313. // The file should not be compressed
  314. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  315. // Unmounting the archive also commits the archive header and table of contents
  316. // to the stream
  317. archiveWriter->UnmountArchive();
  318. EXPECT_FALSE(archiveWriter->IsMounted());
  319. // As there is one file in the archive which is less than 512 bytes
  320. // The expected Table of Contents offset would be
  321. // 512 to account for the table of contents + 512 for the single file block that is aligned to the
  322. // next multiple of 512
  323. // So the Table of Contents should start at offset 1024
  324. expectedArchiveHeader.m_tocOffset = ArchiveDefaultBlockAlignment + ArchiveDefaultBlockAlignment;
  325. expectedArchiveHeader.m_fileCount = 1;
  326. // Update the uncompressed sizes for the table of contents based on the number of files in the toc
  327. // plus the number of block lines used by that file
  328. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 1;
  329. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 1;
  330. // The path blob should only contain the single filename
  331. expectedArchiveHeader.m_tocPathBlobUncompressedSize = static_cast<AZ::u32>(fileSettings.m_relativeFilePath.Native().size());
  332. // As the file is not compressed the block offset table should not have an entry in it
  333. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = 0;
  334. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  335. // The ArchiveHeader is written out as a 512-byte aligned objefct
  336. // trunacate the span to the size the ArchiveHeader
  337. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  338. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  339. }
  340. // Make a copy of archive buffer after being unmounted
  341. auto copiedArchiveBuffer = archiveBuffer;
  342. // Reset the archive StreamPtr and remount it
  343. {
  344. archiveStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  345. archiveStreamPtr.reset(&archiveStream);
  346. EXPECT_TRUE(archiveWriter->MountArchive(AZStd::move(archiveStreamPtr)));
  347. EXPECT_TRUE(archiveWriter->IsMounted());
  348. archiveWriter->UnmountArchive();
  349. // Verify that the archive contents are the same after the Commit call
  350. // via the second UnmountArchive operation
  351. EXPECT_EQ(copiedArchiveBuffer, archiveBuffer);
  352. }
  353. // Mount the archive a third time, this time remove a file
  354. {
  355. archiveStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN);
  356. archiveStreamPtr.reset(&archiveStream);
  357. EXPECT_TRUE(archiveWriter->MountArchive(AZStd::move(archiveStreamPtr)));
  358. EXPECT_TRUE(archiveWriter->IsMounted());
  359. // Remove using file path
  360. ArchiveRemoveFileResult removeResult = archiveWriter->RemoveFileFromArchive("sanity/test.txt");
  361. EXPECT_TRUE(removeResult);
  362. // Unmount the archive again to update the header and TOC
  363. archiveWriter->UnmountArchive();
  364. // Updated the expected archive header to account for the removed file
  365. // The file block data has not been removed, so the Table of Contents
  366. // offset hasn't changed.
  367. // However the file count and the table of content uncompressed sizes
  368. // should been reduced back to 0.
  369. expectedArchiveHeader.m_fileCount = 0;
  370. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = 0;
  371. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = 0;
  372. expectedArchiveHeader.m_tocPathBlobUncompressedSize = 0;
  373. // As a file has been removed, the deleted block offset table has been updated
  374. // It should point to the lowest offset where a deleted file was located
  375. expectedArchiveHeader.m_firstDeletedBlockOffset = removeResult.m_offset;
  376. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  377. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  378. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  379. }
  380. }
  381. TEST_F(ArchiveWriterFixture, Archive_AddAndRemoveContentFile_InSameMountedArchive_Succeeds)
  382. {
  383. AZStd::vector<AZStd::byte> archiveBuffer;
  384. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  385. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  386. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  387. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  388. ASSERT_TRUE(createArchiveWriterResult);
  389. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  390. AZStd::array<ArchiveAddFileResult, 2> addedFileResults;
  391. {
  392. // Add an uncompressed file to archive
  393. AZStd::string_view fileContent = "Hello World";
  394. ArchiveWriterFileSettings fileSettings;
  395. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  396. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  397. EXPECT_TRUE(addFileResult);
  398. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  399. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  400. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  401. AZStd::to_lower(loweredCaseFilePath.Native());
  402. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  403. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  404. addedFileResults[0] = AZStd::move(addFileResult);
  405. }
  406. {
  407. // Add a compressed file to archive
  408. AZStd::string_view fileContent = "Goodbye World";
  409. ArchiveWriterFileSettings fileSettings;
  410. fileSettings.m_compressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  411. fileSettings.m_relativeFilePath = "Sanity/chess.txt";
  412. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  413. EXPECT_TRUE(addFileResult);
  414. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  415. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  416. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  417. AZStd::to_lower(loweredCaseFilePath.Native());
  418. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  419. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), addFileResult.m_compressionAlgorithm);
  420. addedFileResults[1] = AZStd::move(addFileResult);
  421. }
  422. // Commit the archive header and table of contents to the stream
  423. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  424. ASSERT_TRUE(commitResult);
  425. // Since the header is aligned on 512-byte boundary + the first 2 files
  426. // are aligned on a 512-byte boundary the TOC start offset is
  427. // 512 + (2 * 512)
  428. AZ::u64 tocStartOffset = ArchiveDefaultBlockAlignment +
  429. (ArchiveDefaultBlockAlignment * addedFileResults.size());
  430. // The Archive should contain 2 files
  431. {
  432. ArchiveHeader expectedArchiveHeader;
  433. expectedArchiveHeader.m_fileCount = 2;
  434. expectedArchiveHeader.m_tocOffset = tocStartOffset;
  435. // Update the uncompressed sizes for the table of contents based on the number of files in the TOC
  436. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 2;
  437. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 2;
  438. // The path blob should contain the both file paths back-to-back with bytes in-between
  439. AZ::u32 filePathBlobTableSize = static_cast<AZ::u32>(addedFileResults[0].m_relativeFilePath.Native().size());
  440. filePathBlobTableSize += static_cast<AZ::u32>(addedFileResults[1].m_relativeFilePath.Native().size());
  441. expectedArchiveHeader.m_tocPathBlobUncompressedSize = filePathBlobTableSize;
  442. // Since one of the files are compressed and is under 6 MiB, there should be 1 block line in
  443. // the block offset table
  444. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = sizeof(ArchiveBlockLineUnion) * 1;
  445. // Since a compression algorithm is being used set the first entry in the ArchiveHeader m_compressionAlgorithmIds array
  446. size_t currentCompressionAlgorithmIndex{};
  447. expectedArchiveHeader.m_compressionAlgorithmsIds[currentCompressionAlgorithmIndex++] = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  448. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  449. // The ArchiveHeader is written out as a 512-byte aligned
  450. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  451. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  452. }
  453. // Now remove the compressed file from the Archive
  454. ArchiveRemoveFileResult removeFileResult;
  455. {
  456. ArchiveAddFileResult& compressedFileAddResult = addedFileResults[1];
  457. // Use the token to remove the file in this case
  458. removeFileResult = archiveWriter->RemoveFileFromArchive(compressedFileAddResult.m_filePathToken);
  459. EXPECT_TRUE(removeFileResult);
  460. }
  461. // Commit the update archive header and table of contents to stream
  462. commitResult = archiveWriter->Commit();
  463. ASSERT_TRUE(commitResult);
  464. // The Archive should now only contain 1 file
  465. {
  466. ArchiveHeader expectedArchiveHeader;
  467. expectedArchiveHeader.m_fileCount = 1;
  468. // NOTE: Removing a file that does remove it's deleted blocks from the archive data
  469. // Therefore the TOC Offset never decreases
  470. expectedArchiveHeader.m_tocOffset = expectedArchiveHeader.m_tocOffset = tocStartOffset;
  471. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 1;
  472. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 1;
  473. // The path blob should contain only the uncompressed file
  474. AZ::u32 filePathBlobTableSize = static_cast<AZ::u32>(addedFileResults[0].m_relativeFilePath.Native().size());
  475. expectedArchiveHeader.m_tocPathBlobUncompressedSize = filePathBlobTableSize;
  476. // The block offset table DOES NOT remove deleted entries in order to improve performance
  477. // So there should still be a single block line entry in the table
  478. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = sizeof(ArchiveBlockLineUnion) * 1;
  479. // The compression algorithm IDs registered in the ArchiveHeader are not removed form the archive
  480. // If the desire is to clear out unneeded compression algorithm ID, then a new archive can be
  481. // created and the contents of the old archive should be copied to it
  482. size_t currentCompressionAlgorithmIndex{};
  483. expectedArchiveHeader.m_compressionAlgorithmsIds[currentCompressionAlgorithmIndex++] = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  484. // As the compressed file was removed, the TOC first deleted block offset is updated to point
  485. // to the offset of the lowest deleted file block
  486. // Use the remove file result local to update the expected value with that offset
  487. expectedArchiveHeader.m_firstDeletedBlockOffset = removeFileResult.m_offset;
  488. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  489. // The ArchiveHeader is written out as a 512-byte aligned
  490. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  491. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  492. }
  493. // Now remove the uncompressed file from the Archive
  494. {
  495. ArchiveAddFileResult& uncompressedFileAddResult = addedFileResults[0];
  496. // NOTE: This time use the relative file path
  497. removeFileResult = archiveWriter->RemoveFileFromArchive(uncompressedFileAddResult.m_filePathToken);
  498. EXPECT_TRUE(removeFileResult);
  499. }
  500. // Commit the update archive header and table of contents to stream
  501. commitResult = archiveWriter->Commit();
  502. ASSERT_TRUE(commitResult);
  503. // The Archive should now be empty
  504. {
  505. ArchiveHeader expectedArchiveHeader;
  506. expectedArchiveHeader.m_fileCount = 0;
  507. // NOTE: Removing a file that does remove it's deleted blocks from the archive data
  508. // Therefore the TOC Offset never decreases
  509. expectedArchiveHeader.m_tocOffset = expectedArchiveHeader.m_tocOffset = tocStartOffset;
  510. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = 0;
  511. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = 0;
  512. // The path blob table should now be empty
  513. AZ::u32 filePathBlobTableSize = 0;
  514. expectedArchiveHeader.m_tocPathBlobUncompressedSize = filePathBlobTableSize;
  515. // The block offset table DOES NOT remove deleted entries in order to improve performance
  516. // So there should still be a single block line entry in the table
  517. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = sizeof(ArchiveBlockLineUnion) * 1;
  518. // The compression algorithm IDs registered in the ArchiveHeader are not removed form the archive
  519. // If the desire is to clear out unneeded compression algorithm ID, then a new archive can be
  520. // created and the contents of the old archive should be copied to it
  521. size_t currentCompressionAlgorithmIndex{};
  522. expectedArchiveHeader.m_compressionAlgorithmsIds[currentCompressionAlgorithmIndex++] = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  523. // The Deleted block offset should now be 0x200 as the all the files in the archive have been deleted
  524. expectedArchiveHeader.m_firstDeletedBlockOffset = removeFileResult.m_offset;
  525. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  526. // The ArchiveHeader is written out as a 512-byte aligned
  527. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  528. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  529. }
  530. // Finally add back a file with the same name as the compressed file
  531. {
  532. // Add a compressed file to archive
  533. AZStd::string_view fileContent = "Big World";
  534. ArchiveWriterFileSettings fileSettings;
  535. fileSettings.m_compressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  536. fileSettings.m_relativeFilePath = "Sanity/chess.txt";
  537. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  538. EXPECT_TRUE(addFileResult);
  539. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  540. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  541. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  542. AZStd::to_lower(loweredCaseFilePath.Native());
  543. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  544. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), addFileResult.m_compressionAlgorithm);
  545. addedFileResults[1] = AZStd::move(addFileResult);
  546. }
  547. // Commit the update archive header and table of contents to stream
  548. commitResult = archiveWriter->Commit();
  549. ASSERT_TRUE(commitResult);
  550. // The should be back at 1 file again
  551. {
  552. ArchiveHeader expectedArchiveHeader;
  553. expectedArchiveHeader.m_fileCount = 1;
  554. // NOTE: Removing a file that does remove it's deleted blocks from the archive data
  555. // Therefore the TOC Offset never decreases
  556. expectedArchiveHeader.m_tocOffset = expectedArchiveHeader.m_tocOffset = tocStartOffset;
  557. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 1;
  558. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 1;
  559. // The path blob should contain only the uncompressed file
  560. AZ::u32 filePathBlobTableSize = static_cast<AZ::u32>(addedFileResults[1].m_relativeFilePath.Native().size());
  561. expectedArchiveHeader.m_tocPathBlobUncompressedSize = filePathBlobTableSize;
  562. // Since a second compressed file have been added
  563. // the TOC block offset table uncompressed size should have grown by 1 block line
  564. // As existing block lines aren't reused in the TOC, it is ever increasing
  565. expectedArchiveHeader.m_tocBlockOffsetTableUncompressedSize = sizeof(ArchiveBlockLineUnion) * 2;
  566. // The compression algorithm IDs registered in the ArchiveHeader are not removed form the archive
  567. // If the desire is to clear out unneeded compression algorithm ID, then a new archive can be
  568. // created and the contents of the old archive should be copied to it
  569. size_t currentCompressionAlgorithmIndex{};
  570. expectedArchiveHeader.m_compressionAlgorithmsIds[currentCompressionAlgorithmIndex++] = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  571. // The deleted block offset should now be 0x400 as the previous deleted block offset of 0x200
  572. // had the compressed data written to it from the string of "Big World"
  573. // That compressed data is less than 512-bytes, therefore the next deleted block offset
  574. // is rounded up to the nearest multiple of 512 which is 0x400 or 1024
  575. expectedArchiveHeader.m_firstDeletedBlockOffset = removeFileResult.m_offset + ArchiveDefaultBlockAlignment;
  576. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  577. // The ArchiveHeader is written out as a 512-byte aligned
  578. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  579. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  580. }
  581. }
  582. TEST_F(ArchiveWriterFixture, AddFileToArchive_CanUpdateExistingFile_Succeeds)
  583. {
  584. AZStd::vector<AZStd::byte> archiveBuffer;
  585. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  586. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  587. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  588. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  589. ASSERT_TRUE(createArchiveWriterResult);
  590. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  591. // Add an compressed file to archive
  592. AZStd::string_view fileContent = "Hello World";
  593. ArchiveWriterFileSettings fileSettings;
  594. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  595. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  596. EXPECT_TRUE(addFileResult);
  597. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  598. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  599. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  600. AZStd::to_lower(loweredCaseFilePath.Native());
  601. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  602. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  603. // Write the archive header and table of contents to stream
  604. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  605. ASSERT_TRUE(commitResult);
  606. // There should be a single file in the archive
  607. {
  608. ArchiveHeader expectedArchiveHeader;
  609. expectedArchiveHeader.m_tocOffset = ArchiveDefaultBlockAlignment + ArchiveDefaultBlockAlignment;
  610. expectedArchiveHeader.m_fileCount = 1;
  611. expectedArchiveHeader.m_tocFileMetadataTableUncompressedSize = sizeof(ArchiveTocFileMetadata) * 1;
  612. expectedArchiveHeader.m_tocPathIndexTableUncompressedSize = sizeof(ArchiveTocFilePathIndex) * 1;
  613. // The path blob should contain only the compressed file path
  614. AZ::u32 filePathBlobTableSize = static_cast<AZ::u32>(addFileResult.m_relativeFilePath.Native().size());
  615. expectedArchiveHeader.m_tocPathBlobUncompressedSize = filePathBlobTableSize;
  616. auto expectedArchiveHeaderSpan = AZStd::span(reinterpret_cast<AZStd::byte*>(&expectedArchiveHeader), sizeof(expectedArchiveHeader));
  617. // The ArchiveHeader is written out as a 512-byte aligned
  618. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  619. EXPECT_TRUE(AZStd::ranges::equal(archiveBufferSpan, expectedArchiveHeaderSpan));
  620. }
  621. // Update the existing file and add compression to it
  622. fileSettings.m_fileMode = ArchiveWriterFileMode::AddNewOrUpdateExisting;
  623. fileSettings.m_compressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  624. fileContent = "Big Little Tea Time";
  625. addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  626. EXPECT_TRUE(addFileResult);
  627. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  628. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  629. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), addFileResult.m_compressionAlgorithm);
  630. // Cast the first sizeof(ArchiveHeader) bytes to an ArchiveHeader
  631. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  632. auto archiveHeader = reinterpret_cast<ArchiveHeader*>(archiveBufferSpan.data());
  633. // Write toe updated archive header and table of contents out
  634. commitResult = archiveWriter->Commit();
  635. ASSERT_TRUE(commitResult);
  636. // Compare against the known expected values
  637. EXPECT_EQ(1, archiveHeader->m_fileCount);
  638. EXPECT_EQ(sizeof(ArchiveTocFileMetadata) * 1, archiveHeader->m_tocFileMetadataTableUncompressedSize);
  639. EXPECT_EQ(sizeof(ArchiveTocFilePathIndex) * 1, archiveHeader->m_tocPathIndexTableUncompressedSize);
  640. EXPECT_EQ(fileSettings.m_relativeFilePath.Native().size(), archiveHeader->m_tocPathBlobUncompressedSize);
  641. EXPECT_EQ(sizeof(ArchiveBlockLineUnion), archiveHeader->m_tocBlockOffsetTableUncompressedSize);
  642. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), archiveHeader->m_compressionAlgorithmsIds[0]);
  643. }
  644. TEST_F(ArchiveWriterFixture, FileWithSizeGreaterThanMaxBlockLineSize_WritesMultipleBlockLines_WhenUsingCompression_ToBlockOffsetTable)
  645. {
  646. using namespace Archive::literals;
  647. // Generate a 7 MiB file
  648. AZStd::vector<AZStd::byte> largeFileBuffer;
  649. largeFileBuffer.resize_no_construct(7_mib);
  650. // Lambda is marked as mutable to allow the member `currentValue`
  651. // capture to be modified in place
  652. auto RepeatingByteSequenceGenerator = [currentValue = 0]() mutable
  653. {
  654. return static_cast<AZStd::byte>(currentValue % 256);
  655. };
  656. AZStd::generate(largeFileBuffer.begin(), largeFileBuffer.end(),
  657. RepeatingByteSequenceGenerator);
  658. // Create the Archive stream
  659. AZStd::vector<AZStd::byte> archiveBuffer;
  660. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  661. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  662. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  663. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  664. ASSERT_TRUE(createArchiveWriterResult);
  665. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  666. // Add a compressed file to archive
  667. ArchiveWriterFileSettings fileSettings;
  668. fileSettings.m_compressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  669. fileSettings.m_relativeFilePath = "Sanity/chess.txt";
  670. auto addFileResult = archiveWriter->AddFileToArchive(largeFileBuffer, fileSettings);
  671. EXPECT_TRUE(addFileResult);
  672. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  673. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  674. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  675. AZStd::to_lower(loweredCaseFilePath.Native());
  676. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  677. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), addFileResult.m_compressionAlgorithm);
  678. // Commit the archive writer to update the header and table of contents in the stream
  679. ASSERT_TRUE(archiveWriter->Commit());
  680. // Cast the first sizeof(ArchiveHeader) bytes to an ArchiveHeader
  681. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  682. auto archiveHeader = reinterpret_cast<ArchiveHeader*>(archiveBufferSpan.data());
  683. // Compare against the known expected values
  684. EXPECT_EQ(1, archiveHeader->m_fileCount);
  685. EXPECT_EQ(sizeof(ArchiveTocFileMetadata) * 1, archiveHeader->m_tocFileMetadataTableUncompressedSize);
  686. EXPECT_EQ(sizeof(ArchiveTocFilePathIndex) * 1, archiveHeader->m_tocPathIndexTableUncompressedSize);
  687. EXPECT_EQ(fileSettings.m_relativeFilePath.Native().size(), archiveHeader->m_tocPathBlobUncompressedSize);
  688. // Since the file being written is 7 MiB uncompressed it uses 4 2-MiB blocks for compression
  689. // (Well really 3 2-MiB blocks and the remaining block caps out at 1 MiB)
  690. // Since a block line can only represent 3 2-MiB block, multiple block lines must be written
  691. EXPECT_EQ(sizeof(ArchiveBlockLineUnion) * GetBlockLineCountIfCompressed(static_cast<AZ::u64>(largeFileBuffer.size())),
  692. archiveHeader->m_tocBlockOffsetTableUncompressedSize);
  693. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), archiveHeader->m_compressionAlgorithmsIds[0]);
  694. }
  695. TEST_F(ArchiveWriterFixture, CanWriteCompressedTOC_AndReadCompressedBackIn_Succeeds)
  696. {
  697. AZStd::vector<AZStd::byte> archiveBuffer;
  698. AZ::IO::ByteContainerStream archiveStream(&archiveBuffer);
  699. // Set the ArchiveStreamDeleter to not delete the stack ByteContainerStream
  700. IArchiveWriter::ArchiveStreamPtr archiveStreamPtr(&archiveStream, { false });
  701. ArchiveWriterSettings writerSettings;
  702. writerSettings.m_tocCompressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  703. auto createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr),
  704. writerSettings);
  705. ASSERT_TRUE(createArchiveWriterResult);
  706. AZStd::unique_ptr<IArchiveWriter> archiveWriter = AZStd::move(createArchiveWriterResult.value());
  707. // Add an compressed file to archive
  708. AZStd::string_view fileContent = "Hello World";
  709. ArchiveWriterFileSettings fileSettings;
  710. fileSettings.m_relativeFilePath = "Sanity/test.txt";
  711. auto addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  712. EXPECT_TRUE(addFileResult);
  713. EXPECT_NE(InvalidArchiveFileToken, addFileResult.m_filePathToken);
  714. // the ArchiveWriterFileSettings defaults to lowercasing paths added to the archive
  715. AZ::IO::Path loweredCaseFilePath = fileSettings.m_relativeFilePath;
  716. AZStd::to_lower(loweredCaseFilePath.Native());
  717. EXPECT_EQ(loweredCaseFilePath, addFileResult.m_relativeFilePath);
  718. EXPECT_EQ(Compression::Uncompressed, addFileResult.m_compressionAlgorithm);
  719. // Write the archive header and the compressed table of contents to stream
  720. IArchiveWriter::CommitResult commitResult = archiveWriter->Commit();
  721. ASSERT_TRUE(commitResult);
  722. // Write the updated archive header and table of contents out
  723. commitResult = archiveWriter->Commit();
  724. ASSERT_TRUE(commitResult);
  725. // Compare against the known expected values
  726. // Verify the Table of Contents is compressed
  727. {
  728. // Cast the first sizeof(ArchiveHeader) bytes to an ArchiveHeader
  729. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  730. auto archiveHeader = reinterpret_cast<ArchiveHeader*>(archiveBufferSpan.data());
  731. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), archiveHeader->m_compressionAlgorithmsIds[0]);
  732. EXPECT_EQ(0, archiveHeader->m_tocCompressionAlgoIndex);
  733. EXPECT_GT(archiveHeader->m_tocCompressedSize, 0);
  734. EXPECT_EQ(1, archiveHeader->m_fileCount);
  735. EXPECT_EQ(sizeof(ArchiveTocFileMetadata) * 1, archiveHeader->m_tocFileMetadataTableUncompressedSize);
  736. EXPECT_EQ(sizeof(ArchiveTocFilePathIndex) * 1, archiveHeader->m_tocPathIndexTableUncompressedSize);
  737. EXPECT_EQ(fileSettings.m_relativeFilePath.Native().size(), archiveHeader->m_tocPathBlobUncompressedSize);
  738. EXPECT_EQ(0, archiveHeader->m_tocBlockOffsetTableUncompressedSize);
  739. }
  740. // Reset the ArchiveWriter instance and make a new instance
  741. // that loads the Archive stream now containing a compressed TOC
  742. archiveWriter.reset();
  743. archiveStreamPtr.reset(&archiveStream);
  744. createArchiveWriterResult = CreateArchiveWriter(AZStd::move(archiveStreamPtr));
  745. ASSERT_TRUE(createArchiveWriterResult);
  746. archiveWriter = AZStd::move(createArchiveWriterResult.value());
  747. // Validate the Table of Contents by updating an existing file
  748. fileSettings.m_fileMode = ArchiveWriterFileMode::AddNewOrUpdateExisting;
  749. fileSettings.m_compressionAlgorithm = CompressionLZ4::GetLZ4CompressionAlgorithmId();
  750. fileContent = "Land data to compress";
  751. addFileResult = archiveWriter->AddFileToArchive(StringToByteSpan(fileContent), fileSettings);
  752. EXPECT_TRUE(addFileResult);
  753. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), addFileResult.m_compressionAlgorithm);
  754. // Re-write the archive header and Table of contents
  755. commitResult = archiveWriter->Commit();
  756. ASSERT_TRUE(commitResult);
  757. // Compare against the known expected values
  758. // Verify the Table of Contents is compressed
  759. // Cast the first sizeof(ArchiveHeader) bytes to an ArchiveHeader
  760. {
  761. auto archiveBufferSpan = AZStd::span(archiveBuffer).first(sizeof(ArchiveHeader));
  762. auto archiveHeader = reinterpret_cast<ArchiveHeader*>(archiveBufferSpan.data());
  763. EXPECT_EQ(CompressionLZ4::GetLZ4CompressionAlgorithmId(), archiveHeader->m_compressionAlgorithmsIds[0]);
  764. EXPECT_EQ(0, archiveHeader->m_tocCompressionAlgoIndex);
  765. EXPECT_GT(archiveHeader->m_tocCompressedSize, 0);
  766. // There should still only be a single file in the archive
  767. // However as the file is compressed now, there should be block line entries
  768. EXPECT_EQ(1, archiveHeader->m_fileCount);
  769. EXPECT_EQ(sizeof(ArchiveTocFileMetadata) * 1, archiveHeader->m_tocFileMetadataTableUncompressedSize);
  770. EXPECT_EQ(sizeof(ArchiveTocFilePathIndex) * 1, archiveHeader->m_tocPathIndexTableUncompressedSize);
  771. EXPECT_EQ(fileSettings.m_relativeFilePath.Native().size(), archiveHeader->m_tocPathBlobUncompressedSize);
  772. EXPECT_EQ(sizeof(ArchiveBlockLineUnion), archiveHeader->m_tocBlockOffsetTableUncompressedSize);
  773. }
  774. }
  775. }