using System; using System.IO; using System.IO.Compression; using System.Text; using UnitTestSharp; namespace Vellum.UnitTests { public class SaveFilesTests : TestFixture { public const int GenerousLimit = 1024 * 1024; public static SaveFiles Files(int maxTextBytes = GenerousLimit) { return new SaveFiles(entryName: "save.lua", maxTextBytes: maxTextBytes); } public static byte[] ZipOf(params string[] entryTexts) { using (var output = new MemoryStream()) { using (var archive = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true)) { for (int i = 0; i < entryTexts.Length; ++i) { using (var writer = new StreamWriter(archive.CreateEntry("entry" + i).Open())) { writer.Write(entryTexts[i]); } } } return output.ToArray(); } } public static string ReadOrNull(byte[] fileBytes, int maxTextBytes = GenerousLimit) { string text; return Files(maxTextBytes).TryReadText(fileBytes, out text) ? text : null; } public class CtorTests : TestFixture { public void KeepsTheEntryNameAndTheLimit() { var files = new SaveFiles(entryName: "game.lua", maxTextBytes: 123); CheckEqual("game.lua", files.EntryName); CheckEqual(123, files.MaxTextBytes); } public void AnEmptyEntryName_Throws() { CheckThrow(typeof(ArgumentException)); new SaveFiles(entryName: "", maxTextBytes: 10); } public void ANullEntryName_Throws() { CheckThrow(typeof(ArgumentException)); new SaveFiles(entryName: null, maxTextBytes: 10); } public void ANegativeLimit_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new SaveFiles(entryName: "save.lua", maxTextBytes: -1); } public void ALimitOfZero_IsAllowed() { CheckEqual(0, new SaveFiles(entryName: "save.lua", maxTextBytes: 0).MaxTextBytes); } } public class IsCompressedTests : TestFixture { public void ACompressedSave_IsTrue() { CheckTrue(SaveFiles.IsCompressed(Files().Compress("return {}"))); } public void PlainText_IsFalse() { CheckFalse(SaveFiles.IsCompressed(Encoding.UTF8.GetBytes("return {}"))); } public void FewerBytesThanTheSignature_IsFalse() { CheckFalse(SaveFiles.IsCompressed(new byte[] { (byte)'P', (byte)'K', 3 })); CheckFalse(SaveFiles.IsCompressed(new byte[0])); } public void AZipWithNoEntries_IsTrue() { CheckTrue(SaveFiles.IsCompressed(ZipOf())); } public void ADifferentFourthByte_IsFalse() { CheckFalse(SaveFiles.IsCompressed(new byte[] { (byte)'P', (byte)'K', 3, 5 })); CheckFalse(SaveFiles.IsCompressed(new byte[] { (byte)'P', (byte)'K', 5, 7 })); } public void ADifferentFirstTwoBytes_IsFalse() { CheckFalse(SaveFiles.IsCompressed(new byte[] { (byte)'Q', (byte)'K', 3, 4 })); CheckFalse(SaveFiles.IsCompressed(new byte[] { (byte)'P', (byte)'L', 3, 4 })); } } public class CompressTests : TestFixture { public void HoldsTheTextAsOneEntryWithTheConfiguredName() { var files = new SaveFiles(entryName: "game.lua", maxTextBytes: GenerousLimit); using (var archive = new ZipArchive( new MemoryStream(files.Compress("return {}")), ZipArchiveMode.Read)) { CheckEqual(1, archive.Entries.Count); CheckEqual("game.lua", archive.Entries[0].FullName); } } public void RepeatedTextIsSmallerThanItself() { string text = new string('x', 10000); CheckGreater(Encoding.UTF8.GetByteCount(text), Files().Compress(text).Length); } } public class TryReadTextTests : TestFixture { public void ACompressedSave_ReadsBackAsTheSameText() { CheckEqual("return { a = 1 }", ReadOrNull(Files().Compress("return { a = 1 }"))); } public void ACompressedSaveWithNonAsciiText_ReadsBackAsTheSameText() { CheckEqual("-- ¼ × €", ReadOrNull(Files().Compress("-- ¼ × €"))); } public void ASaveUnderAnotherEntryName_StillReads() { CheckEqual("return {}", ReadOrNull(ZipOf("return {}"))); } public void PlainText_ReadsAsItIs() { CheckEqual("return {}", ReadOrNull(Encoding.UTF8.GetBytes("return {}"))); } public void PlainTextLongerThanTheLimit_ReadsAsItIs() { CheckEqual("return {}", ReadOrNull(Encoding.UTF8.GetBytes("return {}"), maxTextBytes: 3)); } public void PlainTextWithAByteOrderMark_ReadsWithoutTheMark() { byte[] body = Encoding.UTF8.GetBytes("return {}"); byte[] withMark = new byte[body.Length + 3]; withMark[0] = 0xEF; withMark[1] = 0xBB; withMark[2] = 0xBF; body.CopyTo(withMark, 3); CheckEqual("return {}", ReadOrNull(withMark)); } public void ATruncatedZip_Fails() { byte[] whole = Files().Compress(new string('x', 5000) + "return {}"); byte[] truncated = new byte[whole.Length / 2]; Array.Copy(whole, truncated, truncated.Length); CheckNull(ReadOrNull(truncated)); } public void BytesThatOnlyStartLikeAZip_Fail() { CheckNull(ReadOrNull(new byte[] { (byte)'P', (byte)'K', 3, 4, 0, 0, 0, 0 })); } public void AZipWithNoEntries_Fails() { CheckNull(ReadOrNull(ZipOf())); } public void AZipWithTwoEntries_Fails() { CheckNull(ReadOrNull(ZipOf("return {}", "return {}"))); } public void ATextExactlyAtTheLimit_Reads() { CheckEqual("0123456789", ReadOrNull(Files().Compress("0123456789"), maxTextBytes: 10)); } public void ATextOneByteOverTheLimit_Fails() { CheckNull(ReadOrNull(Files().Compress("0123456789"), maxTextBytes: 9)); } public void ATextOverTheLimitAcrossSeveralReads_Fails() { byte[] zip = Files().Compress(new string('x', 200000)); CheckNull(ReadOrNull(zip, maxTextBytes: 150000)); } public void AFailureLeavesTheTextNull() { string text = "left over"; CheckFalse(Files().TryReadText(ZipOf(), out text)); CheckNull(text); } } } }