Exemplo n.º 1
0
        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);
        }
Exemplo n.º 2
0
        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);
        }
Exemplo n.º 3
0
 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");
     }
 }
Exemplo n.º 4
0
        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);
        }
Exemplo n.º 5
0
        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);
        }
Exemplo n.º 6
0
        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++;
                }
            }
        }
Exemplo n.º 7
0
        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);
            }
        }
Exemplo n.º 8
0
        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();
        }
Exemplo n.º 9
0
        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);
        }
Exemplo n.º 10
0
        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();
        }