static bool TestInstallerVersion(string version, Func <Stream, BinaryReader, FileInfo> parsingFunction, Stream decompressedStream, BinaryReader binaryReader, int fileNumber, long dataStreamLength) { Bio.Debug($"\nTesting installer version {version}\n"); var pos = decompressedStream.Position; for (var i = 0; i < fileNumber; i++) { try { var fileInfo = parsingFunction(decompressedStream, binaryReader); Bio.Debug(string.Format("Node {0} at offset {1}, size: {2}, end: {3}", i, fileInfo.nodeStart, fileInfo.nodeSize, fileInfo.nodeEnd)); if (!fileInfo.IsValid(dataStreamLength)) { Bio.Debug(fileInfo); Bio.Debug("Invalid file info"); decompressedStream.Position = pos; return(false); } if (fileInfo.type != 0) { decompressedStream.Position = fileInfo.nodeEnd; } } catch (EndOfStreamException) { Bio.Debug("End of Stream reached while parsing file list"); decompressedStream.Position = pos; return(false); } } decompressedStream.Position = pos; return(true); }
static FileInfo TryParse40(Stream decompressedStream, BinaryReader binaryReader) { var fileInfo = new FileInfo(decompressedStream.Position, binaryReader.ReadUInt32(), binaryReader.ReadUInt16()); if (fileInfo.type != 0) { return(fileInfo); } decompressedStream.Skip(3); // Empty dummy files are missing file time attributes, // so the node is shorter than usual if (binaryReader.ReadByte() == 0xE2) { decompressedStream.Skip(30); } else { decompressedStream.Skip(14); var uncompressedSize = binaryReader.ReadUInt32(); var offset = binaryReader.ReadUInt32(); var compressedSize = binaryReader.ReadUInt32(); decompressedStream.Skip(4); fileInfo.SetFileInfos(offset, compressedSize, 0, uncompressedSize); fileInfo.SetFileTimes(binaryReader.ReadInt64(), binaryReader.ReadInt64(), binaryReader.ReadInt64()); } ReadFilePath(decompressedStream, binaryReader, fileInfo); Bio.Debug(fileInfo); return(fileInfo); }
public void SetFileTimes(long modified, long accessed, long created) { try { this.modified = DateTime.FromFileTime(modified); this.accessed = DateTime.FromFileTime(accessed); this.created = DateTime.FromFileTime(created); } catch (Exception) { Bio.Debug("Failed to parse file time"); } }
static MemoryStream UnpackStream(BinaryReader binaryReader, uint blockSize, uint decompressedSize = 0, byte compressionMethod = byte.MaxValue) { if (decompressedSize == 0) { decompressedSize = binaryReader.ReadUInt32(); } if (compressionMethod == byte.MaxValue) { compressionMethod = binaryReader.ReadByte(); } Bio.Debug("Decompressing " + blockSize + " bytes @ " + binaryReader.BaseStream.Position); Bio.Debug(string.Format("\tCompression: {0}, decompressed size: {1}", (COMPRESSION)compressionMethod, decompressedSize)); blockSize -= 5; var decompressedStream = new MemoryStream((int)decompressedSize); switch ((COMPRESSION)compressionMethod) { case COMPRESSION.NONE: binaryReader.BaseStream.Copy(decompressedStream, (int)blockSize); break; case COMPRESSION.DEFLATE: binaryReader.BaseStream.Skip(2); using (var deflateStream = new DeflateStream(binaryReader.BaseStream, CompressionMode.Decompress, true)) { deflateStream.Copy(decompressedStream, (int)decompressedSize); } break; case COMPRESSION.BZ2: using (var bzip2Stream = new BZip2InputStream(binaryReader.BaseStream)) { bzip2Stream.IsStreamOwner = false; bzip2Stream.Copy(decompressedStream, (int)decompressedSize); } break; default: Bio.Warn("Unknown compression method, data might be encrypted and cannot be unpacked. Skipping block."); decompressedStream.Dispose(); binaryReader.BaseStream.Skip(blockSize); return(null); } //binaryReader.BaseStream.Skip(blockSize); return(decompressedStream); }
static void ParseCommandLine(List <string> args) { convertAssets = args.Remove("--convert") || args.Remove("-c"); if (args.Count < 1) { Bio.Error("Please specify the path to a Godot .pck or .exe file.", Bio.EXITCODE.INVALID_INPUT); } inputFile = args[0]; if (!File.Exists(inputFile)) { Bio.Error("The input file " + inputFile + " does not exist. Please make sure the path is correct.", Bio.EXITCODE.IO_ERROR); } outputDirectory = args.Count > 1? args[1]: Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile)); Bio.Debug("Input file: " + inputFile); Bio.Debug("Output directory: " + outputDirectory); }
static void ExtractFiles(Stream inputStream, BinaryReader binaryReader) { inputStream.Position = dataBlockStartPosition + 4; for (var i = 0; i < filesInfos.Count; i++) { var fileInfo = filesInfos[i]; //Bio.Tout(fileInfo, Bio.LOG_SEVERITY.DEBUG); Bio.Debug(fileInfo); inputStream.Position = dataBlockStartPosition + fileInfo.offset + 4; Bio.Cout(string.Format("{0}/{1}\t{2}", i + 1, filesInfos.Count, fileInfo.path)); //Bio.Cout(inputStream); // Empty files if (fileInfo.uncompressedSize == 0) { using (var ms = new MemoryStream()) { SaveToFile(ms, fileInfo.path); } continue; } try { using (var fileData = UnpackStream(binaryReader, fileInfo.compressedSize - 7, fileInfo.uncompressedSize)) { if (!SaveToFile(fileData, fileInfo.path, fileInfo)) { throw new StreamUnsupportedException("Failed to decompress data"); } } } catch (Exception e) { Bio.Warn("Failed to decompress file, exception was " + e.Message); failedExtractions++; } } }
static void ParseFileList(Stream decompressedStream, long dataStreamLength) { var binaryReader = new BinaryReader(decompressedStream); var fileNumber = binaryReader.ReadUInt16(); filesInfos = new List <FileInfo>(); decompressedStream.Skip(2); // Unknown. Maybe fileNumber is 4 bytes? Bio.Cout("\n" + fileNumber + " files in installer\n"); installerVersion = GetInstallerVersion(decompressedStream, binaryReader, fileNumber, dataStreamLength); Func <Stream, BinaryReader, FileInfo> parsingFunction = TryParse30; if (installerVersion >= 40) { parsingFunction = TryParse40; } else if (installerVersion >= 30) { parsingFunction = TryParse30; } else if (installerVersion >= 20) { parsingFunction = TryParse20; } else { Bio.Error($"Unsupported installer version {installerVersion}. Please send a bug report if you want the file to be supported in a future version.", Bio.EXITCODE.NOT_SUPPORTED); } Bio.Cout($"\nStarting extraction as installer version {installerVersion}\n"); for (var i = 0; i < fileNumber; i++) { var fileInfo = parsingFunction(decompressedStream, binaryReader); Bio.Debug(string.Format("Node {0} at offset {1}, size: {2}, end: {3}", i, fileInfo.nodeStart, fileInfo.nodeSize, fileInfo.nodeEnd)); if (!fileInfo.IsValid(dataStreamLength)) { Bio.Error($"The file could not be extracted as installer version {installerVersion}. Please try to manually set the correct version using the command line switch -v.", Bio.EXITCODE.RUNTIME_ERROR); } #if DEBUG if (dumpBlocks) { using (var ms = new MemoryStream((int)fileInfo.nodeSize)) { decompressedStream.Position = fileInfo.nodeStart; decompressedStream.Copy(ms, (int)fileInfo.nodeSize); decompressedStream.Position = fileInfo.nodeEnd; SaveToFile(ms, "FileMeta" + i + ".bin"); } } #endif if (fileInfo.type != 0) { decompressedStream.Position = fileInfo.nodeEnd; continue; } filesInfos.Add(fileInfo); Bio.Debug(fileInfo); } }
static void Main(string[] args) { const string USAGE = "[<options>...] <installer> [<output_directory>]\n\n" + "<options>:\n" + " -v <version>\tExtract as installer version <version>. Auto-detection might not always work correctly, so it is possible to explicitly set the installer version.\n\n" + " -db\tDump blocks. Save additional installer data like registry changes, license files and the uninstaller. This is considered raw data and might not be readable or usable.\n\n" + " -si\tSimulate extraction without writing files to disk."; Bio.Header("cicdec - A Clickteam Install Creator unpacker", VERSION, "2019-2020", "Extracts files from installers made with Clickteam Install Creator", USAGE); inputFile = ParseCommandLine(args); var inputStream = File.OpenRead(inputFile); var binaryReader = new BinaryReader(inputStream); var inputStreamLength = inputStream.Length; // First we need to find the data section. The simplest way to do so // is searching for the signature 0x77, 0x77, 0x67, 0x54, 0x29, 0x48 var startOffset = FindStartOffset(binaryReader); if (startOffset < 0) { Bio.Error("Failed to find overlay signature.", Bio.EXITCODE.INVALID_INPUT); } Bio.Cout("Starting extraction at offset " + startOffset + "\n"); inputStream.Position = startOffset; // The data section consists of a varying number of data blocks, // whose headers give information about the type of data contained inside. while (inputStream.Position + BLOCK_HEADER_SIZE <= inputStreamLength) { var blockId = binaryReader.ReadUInt16(); inputStream.Skip(2); // unknown var blockSize = binaryReader.ReadUInt32(); var blockType = (BLOCK_TYPE)blockId; var nextBlockPos = inputStream.Position + blockSize; Bio.Cout(string.Format("Reading block 0x{0:X} {1,-16} with size {2}", blockId, (BLOCK_TYPE)blockId, blockSize)); var outputFileName = string.Format("Block 0x{0:X} {1}.bin", blockId, (BLOCK_TYPE)blockId); if (blockType == BLOCK_TYPE.FILE_DATA) { // Data block should always be last, but better be safe and parse all other blocks before dataBlockStartPosition = inputStream.Position; if (dumpBlocks) { using (var ms = inputStream.Extract((int)blockSize)) { SaveToFile(ms, outputFileName); } } continue; } else if (blockType == BLOCK_TYPE.FILE_LIST) { fileListStream = UnpackStream(binaryReader, blockSize); if (fileListStream == null) { Bio.Error("Failed to decompress file list", Bio.EXITCODE.RUNTIME_ERROR); } if (dumpBlocks) { SaveToFile(fileListStream, outputFileName); } fileListStream.MoveToStart(); } else if (dumpBlocks) { using (var decompressedStream = UnpackStream(binaryReader, blockSize)) { SaveToFile(decompressedStream, outputFileName); } } Bio.Debug("Pos: " + inputStream.Position + ", expected: " + nextBlockPos); inputStream.Position = nextBlockPos; } if (fileListStream == null) { Bio.Error("File list could not be read. Please send a bug report if you want the file to be supported in a future version.", Bio.EXITCODE.NOT_SUPPORTED); } // Install Creator supports external data files, instead of integrating // the files into the executable. This actually means the data block is // saved as a separate file, which we just need to read. var dataFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + ".D01"); if (File.Exists(dataFilePath)) { Bio.Debug("External data file found"); using (var dataFileStream = File.OpenRead(dataFilePath)) using (var offsetStream = new OffsetStream(dataFileStream, 4)) // Data files seem to have a 4 byte header using (var concatenatedStream = inputStream.Concatenate(offsetStream)) { ParseFileList(fileListStream, concatenatedStream.Length); using (var concatenatedStreamBinaryReader = new BinaryReader(concatenatedStream)) { ExtractFiles(concatenatedStream, concatenatedStreamBinaryReader); } } } else if (dataBlockStartPosition < 0) { Bio.Error("Could not find data block in installer and there is no external data file. The installer is likely corrupt.", Bio.EXITCODE.RUNTIME_ERROR); } else { ParseFileList(fileListStream, inputStreamLength); ExtractFiles(inputStream, binaryReader); } if (failedExtractions > 0) { if (failedExtractions == filesInfos.Count) { Bio.Error("Extraction failed. The installer is either encrypted or a version, which is currently not supported.", Bio.EXITCODE.NOT_SUPPORTED); } Bio.Warn(failedExtractions + " files failed to extract."); } else { Bio.Cout("All OK"); } Bio.Pause(); }
static string ParseCommandLine(string[] args) { var count = args.Length - 1; if (count < 0) { Bio.Error("No input file specified.", Bio.EXITCODE.INVALID_INPUT); } var path = args[count]; string inputFile; if (File.Exists(path)) { inputFile = path; outputDirectory = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path)); } else { outputDirectory = path; count--; if (count < 0) { Bio.Error("Invalid input file specified.", Bio.EXITCODE.INVALID_INPUT); } inputFile = args[count]; } if (!File.Exists(inputFile)) { Bio.Error("Invalid input file specified.", Bio.EXITCODE.IO_ERROR); } for (var i = 0; i < count; i++) { var arg = args[i]; Bio.Debug("Argument: " + arg); switch (arg) { case "--dumpblocks": case "-db": dumpBlocks = true; break; case "--simulate": case "-si": simulate = true; break; case "--version": case "-v": i++; if (i >= args.Length) { Bio.Error("No installer version specified.", Bio.EXITCODE.INVALID_PARAMETER); } try { installerVersion = Convert.ToInt32(args[i]); } catch (FormatException) { Bio.Error("Invalid installer version specified.", Bio.EXITCODE.INVALID_PARAMETER); } break; default: Bio.Warn("Unknown command line option: " + arg); break; } } Bio.Debug("Input file: " + inputFile); Bio.Debug("Output directory: " + outputDirectory); Bio.Debug("Dump blocks: " + dumpBlocks); return(inputFile); }
static void Main(string[] args) { Bio.Header("godotdec", VERSION, "2018-2020", "A simple unpacker for Godot Engine package files (.pck|.exe)", "[<options>] <input_file> [<output_directory>]\n\nOptions:\n-c\t--convert\tConvert textures and audio files"); if (Bio.HasCommandlineSwitchHelp(args)) { return; } ParseCommandLine(args.ToList()); var failed = 0; using (var inputStream = new BinaryReader(File.Open(inputFile, FileMode.Open))) { if (inputStream.ReadInt32() != MAGIC) { inputStream.BaseStream.Seek(-4, SeekOrigin.End); CheckMagic(inputStream.ReadInt32()); inputStream.BaseStream.Skip(-12); var offset = inputStream.ReadInt64(); inputStream.BaseStream.Skip(-offset - 8); CheckMagic(inputStream.ReadInt32()); } Bio.Cout($"Godot Engine version: {inputStream.ReadInt32()}.{inputStream.ReadInt32()}.{inputStream.ReadInt32()}.{inputStream.ReadInt32()}"); // Skip reserved bytes (16x Int32) inputStream.BaseStream.Skip(16 * 4); var fileCount = inputStream.ReadInt32(); Bio.Cout($"Found {fileCount} files in package"); Bio.Cout("Reading file index"); var fileIndex = new List <FileEntry>(); for (var i = 0; i < fileCount; i++) { var pathLength = inputStream.ReadInt32(); var path = Encoding.UTF8.GetString(inputStream.ReadBytes(pathLength)); var fileEntry = new FileEntry(path.ToString(), inputStream.ReadInt64(), inputStream.ReadInt64()); fileIndex.Add(fileEntry); Bio.Debug(fileEntry); inputStream.BaseStream.Skip(16); //break; } if (fileIndex.Count < 1) { Bio.Error("No files were found inside the archive", Bio.EXITCODE.RUNTIME_ERROR); } fileIndex.Sort((a, b) => (int)(a.offset - b.offset)); var fileIndexEnd = inputStream.BaseStream.Position; for (var i = 0; i < fileIndex.Count; i++) { var fileEntry = fileIndex[i]; Bio.Progress(fileEntry.path, i + 1, fileIndex.Count); //break; if (fileEntry.offset < fileIndexEnd) { Bio.Warn("Invalid file offset: " + fileEntry.offset); continue; } // TODO: Only PNG compression is supported if (convertAssets) { // https://github.com/godotengine/godot/blob/master/editor/import/resource_importer_texture.cpp#L222 if (fileEntry.path.EndsWith(".stex") && fileEntry.path.Contains(".png")) { fileEntry.Resize(32); fileEntry.ChangeExtension(".stex", ".png"); Bio.Debug(fileEntry); } // https://github.com/godotengine/godot/blob/master/core/io/resource_format_binary.cpp#L836 else if (fileEntry.path.EndsWith(".oggstr")) { fileEntry.Resize(279, 4); fileEntry.ChangeExtension(".oggstr", ".ogg"); } // https://github.com/godotengine/godot/blob/master/scene/resources/audio_stream_sample.cpp#L518 else if (fileEntry.path.EndsWith(".sample")) { // TODO Bio.Warn("The file type '.sample' is currently not supported"); } } inputStream.BaseStream.Position = fileEntry.offset; var destination = Path.Combine(outputDirectory, fileEntry.path); try { Action <Stream, Stream> copyFunction = (input, output) => input.Copy(output, (int)fileEntry.size); inputStream.BaseStream.WriteToFile(destination, PROMPT_ID, copyFunction); } catch (Exception e) { Bio.Error(e); failed++; } } } Bio.Cout(); Bio.Cout(failed < 1? "All OK": failed + " files failed to extract"); Bio.Pause(); }