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