ArchiveMap.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. using K4os.Compression.LZ4;
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. namespace OpenVIII
  8. {
  9. public class ArchiveMap : IReadOnlyDictionary<string, FI>
  10. {
  11. #region Fields
  12. private readonly Dictionary<string, FI> _entries;
  13. #endregion Fields
  14. #region Constructors
  15. public ArchiveMap(StreamWithRangeValues fI, StreamWithRangeValues fL, int max)
  16. {
  17. if (fI == null || fL == null)
  18. throw new ArgumentNullException($"{nameof(ArchiveMap)}::{nameof(ArchiveMap)} {nameof(fI)} or {nameof(fL)} is null");
  19. Max = max;
  20. var s1 = Uncompress(fL, out var flOffset);
  21. var s2 = Uncompress(fI, out var fiOffset);
  22. var fiSize = fI.UncompressedSize == 0 ? fI.Size : fI.UncompressedSize;
  23. using (var sr = new StreamReader(s1, System.Text.Encoding.UTF8))
  24. using (var br = new BinaryReader(s2))
  25. {
  26. s1.Seek(flOffset, SeekOrigin.Begin);
  27. s2.Seek(fiOffset, SeekOrigin.Begin);
  28. _entries = new Dictionary<string, FI>();
  29. var count = fiSize / 12;
  30. while (count-- > 0)
  31. _entries.Add(sr.ReadLine()?.TrimEnd() ?? throw new InvalidOperationException(), Extended.ByteArrayToClass<FI>(br.ReadBytes(12)));
  32. }
  33. CorrectCompressionForLzsFiles();
  34. }
  35. public ArchiveMap(int count) => _entries = new Dictionary<string, FI>(count);
  36. #endregion Constructors
  37. #region Properties
  38. public int Count => ((IReadOnlyDictionary<string, FI>)_entries).Count;
  39. public IEnumerable<string> Keys => ((IReadOnlyDictionary<string, FI>)FilteredEntries).Keys;
  40. public int Max { get; }
  41. public IReadOnlyList<KeyValuePair<string, FI>> OrderedByName => FilteredEntries.OrderBy(x => x.Key).ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase).ToList();
  42. public IReadOnlyList<KeyValuePair<string, FI>> OrderedByOffset => FilteredEntries.OrderBy(x => x.Value.Offset).ThenBy(x => x.Key).ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase).ToList();
  43. public IEnumerable<FI> Values => ((IReadOnlyDictionary<string, FI>)FilteredEntries).Values;
  44. private IEnumerable<KeyValuePair<string, FI>> FilteredEntries => _entries.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Value.UncompressedSize > 0);
  45. #endregion Properties
  46. #region Indexers
  47. public FI this[string key] => ((IReadOnlyDictionary<string, FI>)_entries)[key];
  48. #endregion Indexers
  49. #region Methods
  50. public static byte[] Lz4Uncompress(byte[] input, int fsUncompressedSize, int offset = 12)
  51. {
  52. Memory.Log.WriteLine($"{nameof(ArchiveMap)}::{nameof(Lz4Uncompress)} :: decompressing data");
  53. //ReadOnlySpan<byte> input = new ReadOnlySpan<byte>(file);
  54. var output = new byte[fsUncompressedSize];
  55. //Span<byte> output = new Span<byte>(r);
  56. while (input.Length - offset > 0 && LZ4Codec.Decode(input, offset, input.Length - offset, output, 0, output.Length) <= -1)
  57. {
  58. offset++;
  59. }
  60. if (offset > -1)
  61. {
  62. Memory.Log.WriteLine($"{nameof(ArchiveWorker)}::{nameof(Lz4Uncompress)}::{nameof(offset)}: {offset}");
  63. return output;
  64. }
  65. else
  66. throw new Exception($"{nameof(ArchiveWorker)}::{nameof(Lz4Uncompress)} Failed to uncompress...");
  67. }
  68. public void Add(KeyValuePair<string, FI> keyValuePair) => _entries.Add(keyValuePair.Key, keyValuePair.Value);
  69. public bool ContainsKey(string key) => ((IReadOnlyDictionary<string, FI>)_entries).ContainsKey(key);
  70. public FI FindString(ref string input, out int size)
  71. {
  72. var localInput = input;
  73. var result = OrderedByName.FirstOrDefault(x => x.Key.IndexOf(localInput, StringComparison.OrdinalIgnoreCase) > -1);
  74. if (string.IsNullOrWhiteSpace(result.Key) || result.Value == default)
  75. {
  76. size = 0;
  77. return null;
  78. }
  79. var result2 = OrderedByOffset.FirstOrDefault(x => x.Value.Offset > result.Value.Offset);
  80. if (result2.Value == default)
  81. {
  82. switch (result.Value.CompressionType)
  83. {
  84. case CompressionType.None:
  85. case CompressionType.LZSS_UnknownSize:
  86. size = result.Value.UncompressedSize;
  87. break;
  88. default:
  89. size = Max - result.Value.Offset;
  90. if (size < 0) size = 0; // could be problems.
  91. break;
  92. }
  93. }
  94. else
  95. size = result2.Value.Offset - result.Value.Offset;
  96. input = result.Key;
  97. return result.Value;
  98. }
  99. public byte[] GetBinaryFile(FI fi, Stream data, string input, int size, long offset = 0)
  100. {
  101. var max = data.Length;
  102. if (data is StreamWithRangeValues s)
  103. {
  104. max = s.Max;
  105. }
  106. if (fi == null)
  107. {
  108. Memory.Log.WriteLine($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} failed to extract {input}");
  109. return null;
  110. }
  111. else
  112. Memory.Log.WriteLine($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} extracting {input}");
  113. if (size == 0)
  114. size = checked((int)(max - (fi.Offset + offset)));
  115. byte[] buffer;
  116. using (var br = new BinaryReader(data))
  117. {
  118. br.BaseStream.Seek(fi.Offset + offset, SeekOrigin.Begin);
  119. switch (fi.CompressionType)
  120. {
  121. case CompressionType.LZSS:
  122. case CompressionType.LZSS_UnknownSize:
  123. case CompressionType.LZSS_LZSS:
  124. var readSize = br.ReadInt32();
  125. if (size != readSize + sizeof(int))
  126. throw new InvalidDataException($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} Size inside of lzs {{{readSize}}} doesn't match size calculated by region {{{size}}}.");
  127. size = readSize;
  128. break;
  129. }
  130. buffer = br.ReadBytes(size);
  131. }
  132. switch (fi.CompressionType)
  133. {
  134. case 0:
  135. return buffer;
  136. case CompressionType.LZSS:
  137. return LZSS.DecompressAllNew(buffer, fi.UncompressedSize);
  138. case CompressionType.LZSS_LZSS:
  139. return LZSS.DecompressAllNew(LZSS.DecompressAllNew(buffer, fi.UncompressedSize), 0, true);
  140. case CompressionType.LZSS_UnknownSize:
  141. return LZSS.DecompressAllNew(buffer, 0);
  142. case CompressionType.LZ4:
  143. return Lz4Uncompress(buffer, fi.UncompressedSize);
  144. default:
  145. throw new InvalidDataException($"{nameof(fi.CompressionType)}: {fi.CompressionType} is invalid...");
  146. }
  147. }
  148. public byte[] GetBinaryFile(string input, Stream data)
  149. {
  150. if (data == null)
  151. throw new ArgumentNullException($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} {nameof(data)} cannot be null.");
  152. if (string.IsNullOrWhiteSpace(input))
  153. throw new ArgumentNullException($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} {nameof(input)} cannot be empty or null");
  154. long offset = 0;
  155. if (data.GetType() == typeof(StreamWithRangeValues))
  156. {
  157. var s = (StreamWithRangeValues)data;
  158. data = Uncompress(s, out offset);
  159. //do I need to do something here? :P
  160. }
  161. var fi = FindString(ref input, out var size);
  162. return GetBinaryFile(fi, data, input, size, offset);
  163. }
  164. public IEnumerator<KeyValuePair<string, FI>> GetEnumerator() => ((IReadOnlyDictionary<string, FI>)_entries).GetEnumerator();
  165. IEnumerator IEnumerable.GetEnumerator() => ((IReadOnlyDictionary<string, FI>)_entries).GetEnumerator();
  166. public KeyValuePair<string, FI> GetFileData(string fileName)
  167. {
  168. if (!TryGetValue(fileName, out var value))
  169. return OrderedByName.FirstOrDefault(x => x.Key.IndexOf(fileName, StringComparison.OrdinalIgnoreCase) >= 0);
  170. return new KeyValuePair<string, FI>(fileName, value);
  171. }
  172. /// <summary>
  173. /// Merge two file lists together.
  174. /// </summary>
  175. /// <param name="child">the map being merged.</param>
  176. /// <param name="offsetForFs">offset where the file data is.</param>
  177. public void MergeMaps(ArchiveMap child, int offsetForFs) => _entries.AddRange(child._entries.ToDictionary(x => x.Key, x => x.Value.Adjust(offsetForFs)));
  178. public bool TryGetValue(string key, out FI value) => ((IReadOnlyDictionary<string, FI>)_entries).TryGetValue(key, out value);
  179. /// <summary>
  180. /// This forces the compression to be set to lzss so the file comes out uncompressed.
  181. /// </summary>
  182. private void CorrectCompressionForLzsFiles()
  183. {
  184. var lszFiles = _entries.Where(x => x.Key.EndsWith("lzs", StringComparison.OrdinalIgnoreCase));
  185. // ReSharper disable once PossibleMultipleEnumeration
  186. lszFiles.Where(x => x.Value.CompressionType == CompressionType.None).ForEach(x => _entries[x.Key].CompressionType = CompressionType.LZSS_UnknownSize);
  187. // ReSharper disable once PossibleMultipleEnumeration
  188. lszFiles.Where(x => x.Value.CompressionType == CompressionType.LZSS).ForEach(x => _entries[x.Key].CompressionType = CompressionType.LZSS_LZSS);
  189. }
  190. private Stream Uncompress(StreamWithRangeValues @in, out long offset)
  191. {
  192. offset = 0;
  193. if (@in == null) return null;
  194. byte[] buffer;
  195. byte[] open(int skip = 0)
  196. {
  197. @in.Seek(@in.Offset + skip, SeekOrigin.Begin);
  198. using (var br = new BinaryReader(@in))
  199. return br.ReadBytes(checked((int)(@in.Size - skip)));
  200. }
  201. if (@in.Compression > 0)
  202. switch (@in.Compression)
  203. {
  204. case CompressionType.LZSS:
  205. buffer = open();
  206. var compressedSize = BitConverter.ToInt32(buffer, 0);
  207. if (compressedSize != buffer.Length - sizeof(int))
  208. throw new InvalidDataException($"{nameof(ArchiveMap)}::{nameof(Uncompress)} buffer size incorrect ({compressedSize}) != ({buffer.Length - sizeof(int)})");
  209. return new MemoryStream(LZSS.DecompressAllNew(buffer, @in.UncompressedSize, true));
  210. case CompressionType.LZ4:
  211. buffer = open();
  212. return new MemoryStream(Lz4Uncompress(buffer, @in.UncompressedSize));
  213. }
  214. offset = @in.Offset;
  215. return @in;
  216. }
  217. #endregion Methods
  218. }
  219. }