1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.Collections.Generic;
14 using System.IO;
15 using System.Linq;
16 using ICSharpCode.SharpZipLib.Zip.Compression;
17 
18 namespace OpenRA.Mods.Common.FileFormats
19 {
20 	public sealed class InstallShieldCABCompression
21 	{
22 		const uint MaxFileGroupCount = 71;
23 
24 		enum CABFlags : ushort
25 		{
26 			FileSplit = 0x1,
27 			FileObfuscated = 0x2,
28 			FileCompressed = 0x4,
29 			FileInvalid = 0x8,
30 		}
31 
32 		enum LinkFlags : byte
33 		{
34 			Prev = 0x1,
35 			Next = 0x2
36 		}
37 
38 		struct FileGroup
39 		{
40 			public readonly string Name;
41 			public readonly uint FirstFile;
42 			public readonly uint LastFile;
43 
FileGroupOpenRA.Mods.Common.FileFormats.InstallShieldCABCompression.FileGroup44 			public FileGroup(Stream stream, long offset)
45 			{
46 				var nameOffset = stream.ReadUInt32();
47 				stream.Position += 18;
48 				FirstFile = stream.ReadUInt32();
49 				LastFile = stream.ReadUInt32();
50 
51 				var pos = stream.Position;
52 				stream.Position = offset + nameOffset;
53 				Name = stream.ReadASCIIZ();
54 				stream.Position = pos;
55 			}
56 		}
57 
58 		struct CabDescriptor
59 		{
60 			public readonly long FileTableOffset;
61 			public readonly uint FileTableSize;
62 			public readonly uint FileTableSize2;
63 			public readonly uint DirectoryCount;
64 
65 			public readonly uint FileCount;
66 			public readonly long FileTableOffset2;
67 
CabDescriptorOpenRA.Mods.Common.FileFormats.InstallShieldCABCompression.CabDescriptor68 			public CabDescriptor(Stream stream)
69 			{
70 				FileTableOffset = stream.ReadUInt32();
71 				stream.Position += 4;
72 				FileTableSize = stream.ReadUInt32();
73 				FileTableSize2 = stream.ReadUInt32();
74 				DirectoryCount = stream.ReadUInt32();
75 				stream.Position += 8;
76 				FileCount = stream.ReadUInt32();
77 				FileTableOffset2 = stream.ReadUInt32();
78 			}
79 		}
80 
81 		struct DirectoryDescriptor
82 		{
83 			public readonly string Name;
84 
DirectoryDescriptorOpenRA.Mods.Common.FileFormats.InstallShieldCABCompression.DirectoryDescriptor85 			public DirectoryDescriptor(Stream stream, long nameTableOffset)
86 			{
87 				var nameOffset = stream.ReadUInt32();
88 				var pos = stream.Position;
89 
90 				stream.Position = nameTableOffset + nameOffset;
91 
92 				Name = stream.ReadASCIIZ();
93 				stream.Position = pos;
94 			}
95 		}
96 
97 		struct FileDescriptor
98 		{
99 			public readonly uint Index;
100 			public readonly CABFlags Flags;
101 			public readonly uint ExpandedSize;
102 			public readonly uint CompressedSize;
103 			public readonly uint DataOffset;
104 
105 			public readonly byte[] MD5;
106 			public readonly uint NameOffset;
107 			public readonly ushort DirectoryIndex;
108 			public readonly uint LinkToPrevious;
109 
110 			public readonly uint LinkToNext;
111 			public readonly LinkFlags LinkFlags;
112 			public readonly ushort Volume;
113 			public readonly string Filename;
114 
FileDescriptorOpenRA.Mods.Common.FileFormats.InstallShieldCABCompression.FileDescriptor115 			public FileDescriptor(Stream stream, uint index, long tableOffset)
116 			{
117 				Index = index;
118 				Flags = (CABFlags)stream.ReadUInt16();
119 				ExpandedSize = stream.ReadUInt32();
120 				stream.Position += 4;
121 				CompressedSize = stream.ReadUInt32();
122 
123 				stream.Position += 4;
124 				DataOffset = stream.ReadUInt32();
125 				stream.Position += 4;
126 				MD5 = stream.ReadBytes(16);
127 
128 				stream.Position += 16;
129 				NameOffset = stream.ReadUInt32();
130 				DirectoryIndex = stream.ReadUInt16();
131 				stream.Position += 12;
132 				LinkToPrevious = stream.ReadUInt32();
133 				LinkToNext = stream.ReadUInt32();
134 
135 				LinkFlags = (LinkFlags)stream.ReadUInt8();
136 				Volume = stream.ReadUInt16();
137 
138 				var pos = stream.Position;
139 				stream.Position = tableOffset + NameOffset;
140 				Filename = stream.ReadASCIIZ();
141 				stream.Position = pos;
142 			}
143 		}
144 
145 		struct CommonHeader
146 		{
147 			public const long Size = 16;
148 			public readonly uint Version;
149 			public readonly uint VolumeInfo;
150 			public readonly long CabDescriptorOffset;
151 			public readonly uint CabDescriptorSize;
152 
CommonHeaderOpenRA.Mods.Common.FileFormats.InstallShieldCABCompression.CommonHeader153 			public CommonHeader(Stream stream)
154 			{
155 				Version = stream.ReadUInt32();
156 				VolumeInfo = stream.ReadUInt32();
157 				CabDescriptorOffset = stream.ReadUInt32();
158 				CabDescriptorSize = stream.ReadUInt32();
159 			}
160 		}
161 
162 		struct VolumeHeader
163 		{
164 			public readonly uint DataOffset;
165 			public readonly uint DataOffsetHigh;
166 			public readonly uint FirstFileIndex;
167 			public readonly uint LastFileIndex;
168 
169 			public readonly uint FirstFileOffset;
170 			public readonly uint FirstFileOffsetHigh;
171 			public readonly uint FirstFileSizeExpanded;
172 			public readonly uint FirstFileSizeExpandedHigh;
173 
174 			public readonly uint FirstFileSizeCompressed;
175 			public readonly uint FirstFileSizeCompressedHigh;
176 			public readonly uint LastFileOffset;
177 			public readonly uint LastFileOffsetHigh;
178 
179 			public readonly uint LastFileSizeExpanded;
180 			public readonly uint LastFileSizeExpandedHigh;
181 			public readonly uint LastFileSizeCompressed;
182 			public readonly uint LastFileSizeCompressedHigh;
183 
VolumeHeaderOpenRA.Mods.Common.FileFormats.InstallShieldCABCompression.VolumeHeader184 			public VolumeHeader(Stream stream)
185 			{
186 				DataOffset = stream.ReadUInt32();
187 				DataOffsetHigh = stream.ReadUInt32();
188 
189 				FirstFileIndex = stream.ReadUInt32();
190 				LastFileIndex = stream.ReadUInt32();
191 				FirstFileOffset = stream.ReadUInt32();
192 				FirstFileOffsetHigh = stream.ReadUInt32();
193 
194 				FirstFileSizeExpanded = stream.ReadUInt32();
195 				FirstFileSizeExpandedHigh = stream.ReadUInt32();
196 				FirstFileSizeCompressed = stream.ReadUInt32();
197 				FirstFileSizeCompressedHigh = stream.ReadUInt32();
198 
199 				LastFileOffset = stream.ReadUInt32();
200 				LastFileOffsetHigh = stream.ReadUInt32();
201 				LastFileSizeExpanded = stream.ReadUInt32();
202 				LastFileSizeExpandedHigh = stream.ReadUInt32();
203 
204 				LastFileSizeCompressed = stream.ReadUInt32();
205 				LastFileSizeCompressedHigh = stream.ReadUInt32();
206 			}
207 		}
208 
209 		class CabExtracter
210 		{
211 			readonly FileDescriptor file;
212 			readonly Dictionary<int, Stream> volumes;
213 
214 			uint remainingInArchive;
215 			uint toExtract;
216 
217 			int currentVolumeID;
218 			Stream currentVolume;
219 
CabExtracter(FileDescriptor file, Dictionary<int, Stream> volumes)220 			public CabExtracter(FileDescriptor file, Dictionary<int, Stream> volumes)
221 			{
222 				this.file = file;
223 				this.volumes = volumes;
224 
225 				remainingInArchive = 0;
226 				toExtract = file.Flags.HasFlag(CABFlags.FileCompressed) ? file.CompressedSize : file.ExpandedSize;
227 
228 				SetVolume(file.Volume);
229 			}
230 
CopyTo(Stream output, Action<int> onProgress)231 			public void CopyTo(Stream output, Action<int> onProgress)
232 			{
233 				if (file.Flags.HasFlag(CABFlags.FileCompressed))
234 				{
235 					var inf = new Inflater(true);
236 					var buffer = new byte[165535];
237 					do
238 					{
239 						var bytesToExtract = currentVolume.ReadUInt16();
240 						remainingInArchive -= 2;
241 						toExtract -= 2;
242 						inf.SetInput(GetBytes(bytesToExtract));
243 						toExtract -= bytesToExtract;
244 						while (!inf.IsNeedingInput)
245 						{
246 							if (onProgress != null)
247 								onProgress((int)(100 * output.Position / file.ExpandedSize));
248 
249 							var inflated = inf.Inflate(buffer);
250 							output.Write(buffer, 0, inflated);
251 						}
252 
253 						inf.Reset();
254 					}
255 					while (toExtract > 0);
256 				}
257 				else
258 				{
259 					do
260 					{
261 						if (onProgress != null)
262 							onProgress((int)(100 * output.Position / file.ExpandedSize));
263 
264 						toExtract -= remainingInArchive;
265 						output.Write(GetBytes(remainingInArchive), 0, (int)remainingInArchive);
266 					}
267 					while (toExtract > 0);
268 				}
269 			}
270 
GetBytes(uint count)271 			public byte[] GetBytes(uint count)
272 			{
273 				if (count < remainingInArchive)
274 				{
275 					remainingInArchive -= count;
276 					return currentVolume.ReadBytes((int)count);
277 				}
278 				else
279 				{
280 					var outArray = new byte[count];
281 					var read = currentVolume.Read(outArray, 0, (int)remainingInArchive);
282 					if (toExtract > remainingInArchive)
283 					{
284 						SetVolume(currentVolumeID + 1);
285 						remainingInArchive -= (uint)currentVolume.Read(outArray, read, (int)count - read);
286 					}
287 
288 					return outArray;
289 				}
290 			}
291 
SetVolume(int newVolume)292 			void SetVolume(int newVolume)
293 			{
294 				currentVolumeID = newVolume;
295 				if (!volumes.TryGetValue(currentVolumeID, out currentVolume))
296 					throw new FileNotFoundException("Volume {0} is not available".F(currentVolumeID));
297 
298 				currentVolume.Position = 0;
299 				if (currentVolume.ReadUInt32() != 0x28635349)
300 					throw new InvalidDataException("Not an Installshield CAB package");
301 
302 				uint fileOffset;
303 				if (file.Flags.HasFlag(CABFlags.FileSplit))
304 				{
305 					currentVolume.Position += CommonHeader.Size;
306 					var head = new VolumeHeader(currentVolume);
307 					if (file.Index == head.LastFileIndex)
308 					{
309 						if (file.Flags.HasFlag(CABFlags.FileCompressed))
310 							remainingInArchive = head.LastFileSizeCompressed;
311 						else
312 							remainingInArchive = head.LastFileSizeExpanded;
313 
314 						fileOffset = head.LastFileOffset;
315 					}
316 					else if (file.Index == head.FirstFileIndex)
317 					{
318 						if (file.Flags.HasFlag(CABFlags.FileCompressed))
319 							remainingInArchive = head.FirstFileSizeCompressed;
320 						else
321 							remainingInArchive = head.FirstFileSizeExpanded;
322 
323 						fileOffset = head.FirstFileOffset;
324 					}
325 					else
326 						throw new InvalidDataException("Cannot Resolve Remaining Stream");
327 				}
328 				else
329 				{
330 					if (file.Flags.HasFlag(CABFlags.FileCompressed))
331 						remainingInArchive = file.CompressedSize;
332 					else
333 						remainingInArchive = file.ExpandedSize;
334 
335 					fileOffset = file.DataOffset;
336 				}
337 
338 				currentVolume.Position = fileOffset;
339 			}
340 		}
341 
342 		readonly Dictionary<string, FileDescriptor> index = new Dictionary<string, FileDescriptor>();
343 		readonly Dictionary<int, Stream> volumes;
344 
InstallShieldCABCompression(Stream header, Dictionary<int, Stream> volumes)345 		public InstallShieldCABCompression(Stream header, Dictionary<int, Stream> volumes)
346 		{
347 			this.volumes = volumes;
348 
349 			if (header.ReadUInt32() != 0x28635349)
350 				throw new InvalidDataException("Not an Installshield CAB package");
351 
352 			header.Position += 8;
353 			var cabDescriptorOffset = header.ReadUInt32();
354 			header.Position = cabDescriptorOffset + 12;
355 			var cabDescriptor = new CabDescriptor(header);
356 			header.Position += 14;
357 
358 			var fileGroupOffsets = new uint[MaxFileGroupCount];
359 			for (var i = 0; i < MaxFileGroupCount; i++)
360 				fileGroupOffsets[i] = header.ReadUInt32();
361 
362 			header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset;
363 			var directories = new DirectoryDescriptor[cabDescriptor.DirectoryCount];
364 			for (var i = 0; i < directories.Length; i++)
365 				directories[i] = new DirectoryDescriptor(header, cabDescriptorOffset + cabDescriptor.FileTableOffset);
366 
367 			var fileGroups = new List<FileGroup>();
368 			foreach (var offset in fileGroupOffsets)
369 			{
370 				var nextOffset = offset;
371 				while (nextOffset != 0)
372 				{
373 					header.Position = cabDescriptorOffset + (long)nextOffset + 4;
374 					var descriptorOffset = header.ReadUInt32();
375 					nextOffset = header.ReadUInt32();
376 					header.Position = cabDescriptorOffset + descriptorOffset;
377 
378 					fileGroups.Add(new FileGroup(header, cabDescriptorOffset));
379 				}
380 			}
381 
382 			header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2;
383 			foreach (var fileGroup in fileGroups)
384 			{
385 				for (var i = fileGroup.FirstFile; i <= fileGroup.LastFile; i++)
386 				{
387 					header.Position = cabDescriptorOffset +	cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2 + i * 0x57;
388 					var file = new FileDescriptor(header, i, cabDescriptorOffset + cabDescriptor.FileTableOffset);
389 					var path = "{0}\\{1}\\{2}".F(fileGroup.Name, directories[file.DirectoryIndex].Name, file.Filename);
390 					index[path] = file;
391 				}
392 			}
393 		}
394 
ExtractFile(string filename, Stream output, Action<int> onProgress = null)395 		public void ExtractFile(string filename, Stream output, Action<int> onProgress = null)
396 		{
397 			FileDescriptor file;
398 			if (!index.TryGetValue(filename, out file))
399 				throw new FileNotFoundException(filename);
400 
401 			ExtractFile(file, output, onProgress);
402 		}
403 
ExtractFile(FileDescriptor file, Stream output, Action<int> onProgress = null)404 		void ExtractFile(FileDescriptor file, Stream output, Action<int> onProgress = null)
405 		{
406 			if (file.Flags.HasFlag(CABFlags.FileInvalid))
407 				throw new InvalidDataException("File Invalid");
408 
409 			if (file.LinkFlags.HasFlag(LinkFlags.Prev))
410 			{
411 				var prev = index.Values.First(f => f.Index == file.LinkToPrevious);
412 				ExtractFile(prev, output, onProgress);
413 				return;
414 			}
415 
416 			if (file.Flags.HasFlag(CABFlags.FileObfuscated))
417 				throw new NotImplementedException("Obfuscated files are not supported");
418 
419 			var extracter = new CabExtracter(file, volumes);
420 			extracter.CopyTo(output, onProgress);
421 
422 			if (output.Length != file.ExpandedSize)
423 				throw new InvalidDataException("File expanded to wrong length. Expected = {0}, Got = {1}".F(file.ExpandedSize, output.Length));
424 		}
425 
426 		public IReadOnlyDictionary<int, IEnumerable<string>> Contents
427 		{
428 			get
429 			{
430 				var contents = new Dictionary<int, List<string>>();
431 				foreach (var kv in index)
432 					contents.GetOrAdd(kv.Value.Volume).Add(kv.Key);
433 
434 				return new ReadOnlyDictionary<int, IEnumerable<string>>(contents
435 					.ToDictionary(x => x.Key, x => x.Value.AsEnumerable()));
436 			}
437 		}
438 	}
439 }
440