using System; using System.Collections.Generic; using System.Text; using System.Reflection; using System.IO; using DSDecmp.Formats; using DSDecmp.Formats.Nitro; namespace DSDecmp { public class NewestProgram { #if DEBUG public static string PluginFolder = "./Plugins/Debug"; #else public static string PluginFolder = "./Plugins"; #endif public static void Main(string[] args) { // I/O input: // file -> read from file, save to file // folder [-co] -> read all files from folder, save to folder_dec or folder_cmp (when decomp or comp resp.) (Same filenames) // file newfile -> read from file, save to newfile // folderin folderout [-co] -> read all files from folderin, save to folderout. (Same filenames) // file1 file2 ... -> read file1, file2, etc // file1 file2 ... folderout [-co] -> read file1, file2, etc, save to folderout. (same filenames) // -> when -co is present, all files that could not be (de)compressed will be copied instead. // preambles: // -> decompress input to output using first matched format // -d [-ge] -> decompress input to output using first matched format. If -ge, then guess the extension based on first 4 bytes. // -d [-ge] -f -> decompress input to output using the indicated format. If -ge, then guess the extension based on first 4 bytes. // -c [opt1 opt2 ...] -> compress input to output using the specified format and its options. // built-in formats: // lz10 -> LZ-0x10, found in >= GBA // lz11 -> LZ-0x11, found in >= NDS // lzovl -> LZ-Ovl/Overlay / backwards LZ, found mostly in NDS overlay files. // huff4 -> 4-bit Huffman, found in >= GBA // huff8 -> 8-bit Huffman, found in >= GBA // huff -> any Huffman format. // gba* -> any format natively supported by the GBA // nds* -> any format natively supported by the NDS, but not LZ-Ovl // when compressing, the best format of the selected set is used. when decompression, // only the formats in the selected set are used. if (args.Length == 0) { PrintUsage(); //Console.ReadLine(); return; } if (args[0] == "-c") { if (args.Length <= 2) { Console.WriteLine("Too few arguments."); return; } CompressionFormat format = FirstOrDefault(GetFormat(args[1])); if (format == null) { return; } string[] ioArgs = new string[args.Length - 2]; Array.Copy(args, 2, ioArgs, 0, ioArgs.Length); int optionCount = format.ParseCompressionOptions(ioArgs); string[] realIoArgs = new string[ioArgs.Length - optionCount]; Array.Copy(ioArgs, optionCount, realIoArgs, 0, realIoArgs.Length); Compress(realIoArgs, format); } else if (args[0] == "-d") { if (args.Length <= 1) { PrintUsage(); return; } int ioIdx = 1; bool guessExtension = false; if (args[ioIdx] == "-ge") { guessExtension = true; ioIdx++; } IEnumerable formats = GetAllFormats(false); // we do not need the built-in composite formats to decompress. if (args[ioIdx] == "-f") { if (args.Length <= ioIdx + 2) { Console.WriteLine("Too few arguments."); return; } formats = GetFormat(args[ioIdx + 1]); ioIdx += 2; } if (formats == null) { return; } if (args.Length <= ioIdx) { Console.WriteLine("Too few arguments."); return; } string[] ioArgs = new string[args.Length - ioIdx]; Array.Copy(args, ioIdx, ioArgs, 0, ioArgs.Length); Decompress(ioArgs, formats, guessExtension); } else { Decompress(args, GetAllFormats(false), false); } } #region Usage printer private static void PrintUsage() { Console.WriteLine("DSDecmp - Decompressor for compression formats used on the NDS - by Barubary"); Console.WriteLine(); Console.WriteLine("Usage:\tDSDecmp FMTARGS IOARGS"); Console.WriteLine(); Console.WriteLine("IOARGS can be:"); Console.WriteLine("-------------------------------------------------------------------------------"); Console.WriteLine("file -> read the file, overwrite it."); Console.WriteLine("folder [-co] -> read all files from folder, save to folder_dec"); Console.WriteLine(" or folder_cmp."); Console.WriteLine("file newfile -> read the file, save it to newfile."); Console.WriteLine(" (newfile cannot exist yet)"); Console.WriteLine("folderin folderout [-co] -> read all files from folderin, save to folderout."); Console.WriteLine("file1 file2 ... -> read file1, file2, etc; overwrite them."); Console.WriteLine("file1 file2 ... folderout [-co] -> read file1, file2, etc; save to folderout."); Console.WriteLine(); Console.WriteLine("When -co is present, all files that could not be handled will be copied to the"); Console.WriteLine(" indicated output folder."); Console.WriteLine("-------------------------------------------------------------------------------"); Console.WriteLine(); Console.WriteLine("FMTARGS can be:"); Console.WriteLine("-------------------------------------------------------------------------------"); Console.WriteLine(" -> try to decompress input to output."); Console.WriteLine("-d [-ge] -> try to decompress input to output."); Console.WriteLine("-d [-ge] -f -> try to decompress input to output, using fiven format"); Console.WriteLine("-c [opt1 ...] -> compress intput to output using given format "); Console.WriteLine(" and options."); Console.WriteLine(); Console.WriteLine("When -ge is present, the extension of the output file will be determined by"); Console.WriteLine(" the first 4 bytes of the decompressed data. (of those are alphanuemric ASCII"); Console.WriteLine(" characters)."); Console.WriteLine("-------------------------------------------------------------------------------"); Console.WriteLine("Supported formats:"); Console.WriteLine(" -> description"); foreach (CompressionFormat fmt in GetAllFormats(true)) { Console.WriteLine(fmt.CompressionFlag.PadRight(7, ' ') + "-> " + fmt.Description); } Console.WriteLine("-------------------------------------------------------------------------------"); } #endregion #region Method: Decompress(string[] ioArgs, IEnumerable formats) private static void Decompress(string[] ioArgs, IEnumerable formats, bool guessExtension) { string[] inputFiles; string outputDir; bool copyErrors; if (!ParseIOArguments(ioArgs, false, out inputFiles, out outputDir, out copyErrors)) return; foreach (string input in inputFiles) { string outputFile = outputDir ?? IOUtils.GetParent(input); if (Directory.Exists(outputDir)) outputFile = Path.Combine(outputFile + Path.DirectorySeparatorChar, Path.GetFileName(input)); try { // read the file only once. byte[] inputData; using (Stream inStream = File.OpenRead(input)) { inputData = new byte[inStream.Length]; inStream.Read(inputData, 0, inputData.Length); } bool decompressed = false; foreach (CompressionFormat format in formats) { if (!format.SupportsDecompression) continue; #region try to decompress using the current format using (MemoryStream inStr = new MemoryStream(inputData), outStr = new MemoryStream()) { if (!format.Supports(inStr, inputData.Length)) continue; try { long decompSize = format.Decompress(inStr, inputData.Length, outStr); if (decompSize < 0) continue; if (guessExtension) { string outFileName = Path.GetFileNameWithoutExtension(outputFile); outStr.Position = 0; byte[] magic = new byte[4]; outStr.Read(magic, 0, 4); outStr.Position = 0; outFileName += "." + GuessExtension(magic, Path.GetExtension(outputFile).Substring(1)); outputFile = outputFile.Replace(Path.GetFileName(outputFile), outFileName); } using (FileStream output = File.Create(outputFile)) { outStr.WriteTo(output); } decompressed = true; Console.WriteLine(format.ShortFormatString + "-decompressed " + input + " to " + outputFile); break; } catch (TooMuchInputException tmie) { // a TMIE is fine. let the user know and continue saving the decompressed data. Console.WriteLine(tmie.Message); if (guessExtension) { string outFileName = Path.GetFileNameWithoutExtension(outputFile); outStr.Position = 0; byte[] magic = new byte[4]; outStr.Read(magic, 0, 4); outStr.Position = 0; outFileName += "." + GuessExtension(magic, Path.GetExtension(outputFile).Substring(1)); outputFile = outputFile.Replace(Path.GetFileName(outputFile), outFileName); } using (FileStream output = File.Create(outputFile)) { outStr.WriteTo(output); } decompressed = true; Console.WriteLine(format.ShortFormatString + "-decompressed " + input + " to " + outputFile); break; } catch (Exception) { continue; } } #endregion } if (!decompressed) { #region copy or print and continue if (copyErrors) { Copy(input, outputFile); } else Console.WriteLine("No suitable decompressor found for " + input + "."); #endregion } } catch (FileNotFoundException) { Console.WriteLine("The file " + input + " does not exist."); continue; } catch (Exception ex) { Console.WriteLine("Could not load file " + input + ";"); Console.WriteLine(ex.Message); #if DEBUG Console.WriteLine(ex.StackTrace); #endif } } // end foreach input } #endregion Method: Decompress #region Method: Compress /// /// (Attempts to) Compress the given input to the given output, using the given format. /// /// The I/O arguments from the program input. /// The desired format to compress with. private static void Compress(string[] ioArgs, CompressionFormat format) { if (!format.SupportsCompression) { Console.WriteLine("Cannot compress using " + format.ShortFormatString + "; compression is not supported."); return; } string[] inputFiles; string outputDir; bool copyErrors; if (!ParseIOArguments(ioArgs, true, out inputFiles, out outputDir, out copyErrors)) return; foreach (string input in inputFiles) { string outputFile = outputDir ?? IOUtils.GetParent(input); if (Directory.Exists(outputDir)) outputFile = Path.Combine(outputFile + Path.DirectorySeparatorChar, Path.GetFileName(input)); try { // read the file only once. byte[] inputData; using (Stream inStream = File.OpenRead(input)) { inputData = new byte[inStream.Length]; inStream.Read(inputData, 0, inputData.Length); } #region try to compress using (MemoryStream inStr = new MemoryStream(inputData), outStr = new MemoryStream()) { try { long compSize = format.Compress(inStr, inputData.Length, outStr); if (compSize > 0) { using (FileStream output = File.Create(outputFile)) { outStr.WriteTo(output); } if (format is CompositeFormat) Console.Write((format as CompositeFormat).LastUsedCompressFormatString); else Console.Write(format.ShortFormatString); Console.WriteLine("-compressed " + input + " to " + outputFile); } } catch (Exception ex) { #region copy or print and continue if (copyErrors) { Copy(input, outputFile); } else { Console.WriteLine("Could not " + format.ShortFormatString + "-compress " + input + ";"); Console.WriteLine(ex.Message); #if DEBUG Console.WriteLine(ex.StackTrace); #endif } #endregion } } #endregion } catch (FileNotFoundException) { Console.WriteLine("The file " + input + " does not exist."); continue; } catch (Exception ex) { Console.WriteLine("Could not load file " + input + ";"); Console.WriteLine(ex.Message); #if DEBUG Console.WriteLine(ex.StackTrace); #endif } } // end foreach input } #endregion Method: Compress #region Method: ParseIOArguments /// /// Parses the IO arguments of the input. /// /// The arguments to parse. /// If the arguments are used for compression. If not, decompression is assumed. (used for default output folder name) /// The files to handle as input. /// The directory to save the handled files in. If this is null, /// the files should be overwritten. If this does not exist, it is the output file /// (the input may only contain one file if that si the case). /// If files that cannot be handled (properly) should be copied to the output directory. /// True iff parsing of the arguments succeeded. private static bool ParseIOArguments(string[] ioArgs, bool compress, out string[] inputFiles, out string outputDir, out bool copyErrors) { inputFiles = null; // when null, output dir = input dir. if it does not exist, it is the output file (only possible when only one input file). outputDir = null; copyErrors = false; #region check if the -co flag is present if (ioArgs.Length > 0 && ioArgs[ioArgs.Length - 1] == "-co") { string[] newIoArgs = new string[ioArgs.Length - 1]; Array.Copy(ioArgs, newIoArgs, newIoArgs.Length); ioArgs = newIoArgs; copyErrors = true; } #endregion switch (ioArgs.Length) { case 0: Console.WriteLine("No input file given."); return false; case 1: if (Directory.Exists(ioArgs[0])) { inputFiles = Directory.GetFiles(ioArgs[0]); if (compress) outputDir = Path.GetFullPath(ioArgs[0]) + "_cmp"; else outputDir = Path.GetFullPath(ioArgs[0]) + "_dec"; if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir); break; } else if (File.Exists(ioArgs[0])) { inputFiles = ioArgs; outputDir = null; break; } else { Console.WriteLine("The file " + ioArgs[0] + " does not exist."); return false; } case 2: if (Directory.Exists(ioArgs[0])) { inputFiles = Directory.GetFiles(ioArgs[0]); outputDir = ioArgs[1]; if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir); break; } else if (File.Exists(ioArgs[0])) { if (File.Exists(ioArgs[1])) { inputFiles = ioArgs; outputDir = null; break; } else// if (Directory.Exists(ioArgs[1])) // both nonexisting file and existing directory is handled the same. { inputFiles = new string[] { ioArgs[0] }; outputDir = ioArgs[1]; break; } } else { Console.WriteLine("The file " + ioArgs[0] + " does not exist."); return false; } default: if (File.Exists(ioArgs[ioArgs.Length - 1])) { inputFiles = ioArgs; outputDir = null; break; } else //if (Directory.Exists(ioArgs[ioArgs.Length - 1])) // both existing and nonexisting directories are fine. { outputDir = ioArgs[ioArgs.Length - 1]; inputFiles = new string[ioArgs.Length - 1]; Array.Copy(ioArgs, inputFiles, inputFiles.Length); // but we must make sure the output directory exists. if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir); break; } } return true; } #endregion ParseIOArguments #region Method: GuessExtension(magic, defaultExt) /// /// Guess the extension of a file by looking at the given magic bytes of a file. /// If they are alphanumeric (without accents), they could indicate the type of file. /// If no sensible extension could be found from the magic bytes, the given default extension is returned. /// private static string GuessExtension(byte[] magic, string defaultExt) { string ext = ""; for (int i = 0; i < magic.Length && i < 4; i++) { if ((magic[i] >= 'a' && magic[i] <= 'z') || (magic[i] >= 'A' && magic[i] <= 'Z') || char.IsDigit((char)magic[i])) { ext += (char)magic[i]; } else break; } if (ext.Length <= 1) return defaultExt; return ext; } #endregion /// /// Copies the source file to the destination path. /// private static void Copy(string sourcefile, string destfile) { if (Path.GetFullPath(sourcefile) == Path.GetFullPath(destfile)) return; File.Copy(sourcefile, destfile); Console.WriteLine("Copied " + sourcefile + " to " + destfile); } #region Format sequence getters /// /// Gets the compression format corresponding to the given format string. /// private static IEnumerable GetFormat(string formatstring) { if (formatstring == null) yield break; foreach (CompressionFormat fmt in GetAllFormats(true)) if (fmt.CompressionFlag == formatstring) { yield return fmt; yield break; } Console.WriteLine("No such compression format: " + formatstring); } /// /// Gets a sequence over all compression formats currently supported; both built-in and plugin-based. /// private static IEnumerable GetAllFormats(bool alsoBuiltInCompositeFormats) { foreach (CompressionFormat fmt in GetBuiltInFormats(alsoBuiltInCompositeFormats)) yield return fmt; foreach (CompressionFormat fmt in GetPluginFormats()) yield return fmt; } /// /// Gets a sequence over all built-in compression formats. /// /// If the built-in composite formats should also be part of the sequence. private static IEnumerable GetBuiltInFormats(bool alsoCompositeFormats) { yield return new LZOvl(); yield return new LZ10(); yield return new LZ11(); yield return new Huffman4(); yield return new Huffman8(); yield return new RLE(); yield return new NullCompression(); if (alsoCompositeFormats) { yield return new HuffmanAny(); yield return new CompositeGBAFormat(); yield return new CompositeNDSFormat(); } } /// /// Gets a sequence over all formats that can be used from plugins. /// private static IEnumerable GetPluginFormats() { string pluginPath = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName; pluginPath = Path.Combine(pluginPath, PluginFolder); if (System.IO.Directory.Exists(pluginPath)) { foreach (CompressionFormat fmt in IOUtils.LoadCompressionPlugins(pluginPath)) yield return fmt; } else { Console.WriteLine("Plugin folder " + pluginPath + " is not present; only built-in formats are supported."); } } #endregion /// /// Gets the first item from the given sequence, or the default value of the type in the sequence /// if it is empty. /// private static T FirstOrDefault(IEnumerable sequence) { if (sequence != null) { IEnumerator enumerator = sequence.GetEnumerator(); if (enumerator.MoveNext()) return enumerator.Current; } return default(T); } } }