using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Web; namespace SabreTools.Helper { public class Output { /// /// Create and open an output file for writing direct from a dictionary /// /// All information for creating the datfile header /// Set the output directory /// Logger object for console and/or file output /// True if games should only be compared on game and file name (default), false if system and source are counted /// True if DAT statistics should be output on write, false otherwise (default) /// True if the DAT was written correctly, false otherwise /// /// The following features have been requested for file output: /// - Have the ability to strip special (non-ASCII) characters from rom information /// - Add a flag for ignoring roms with blank sizes /// public static bool WriteDatfile(Dat datdata, string outDir, Logger logger, bool norename = true, bool stats = false) { // Output initial statistics, for kicks if (stats) { Stats.OutputStats(datdata, logger, (datdata.RomCount + datdata.DiskCount == 0)); } // Bucket roms by game name and optionally dedupe SortedDictionary> sortable = DatTools.BucketByGame(datdata.Files, datdata.MergeRoms, norename, logger); // Now write out to file // If it's empty, use the current folder if (outDir.Trim() == "") { outDir = Environment.CurrentDirectory; } // Create the output directory if it doesn't already exist Directory.CreateDirectory(outDir); // Make sure that the three essential fields are filled in if (String.IsNullOrEmpty(datdata.FileName) && String.IsNullOrEmpty(datdata.Name) && String.IsNullOrEmpty(datdata.Description)) { datdata.FileName = datdata.Name = datdata.Description = "Default"; } else if (String.IsNullOrEmpty(datdata.FileName) && String.IsNullOrEmpty(datdata.Name) && !String.IsNullOrEmpty(datdata.Description)) { datdata.FileName = datdata.Name = datdata.Description; } else if (String.IsNullOrEmpty(datdata.FileName) && !String.IsNullOrEmpty(datdata.Name) && String.IsNullOrEmpty(datdata.Description)) { datdata.FileName = datdata.Description = datdata.Name; } else if (String.IsNullOrEmpty(datdata.FileName) && !String.IsNullOrEmpty(datdata.Name) && !String.IsNullOrEmpty(datdata.Description)) { datdata.FileName = datdata.Description; } else if (!String.IsNullOrEmpty(datdata.FileName) && String.IsNullOrEmpty(datdata.Name) && String.IsNullOrEmpty(datdata.Description)) { datdata.Name = datdata.Description = datdata.FileName; } else if (!String.IsNullOrEmpty(datdata.FileName) && String.IsNullOrEmpty(datdata.Name) && !String.IsNullOrEmpty(datdata.Description)) { datdata.Name = datdata.Description; } else if (!String.IsNullOrEmpty(datdata.FileName) && !String.IsNullOrEmpty(datdata.Name) && String.IsNullOrEmpty(datdata.Description)) { datdata.Description = datdata.Name; } else if (!String.IsNullOrEmpty(datdata.FileName) && !String.IsNullOrEmpty(datdata.Name) && !String.IsNullOrEmpty(datdata.Description)) { // Nothing is needed } // Get the outfile name string outfile = Style.CreateOutfileName(outDir, datdata); logger.User("Opening file for writing: " + outfile); try { FileStream fs = File.Create(outfile); StreamWriter sw = new StreamWriter(fs, Encoding.UTF8); // Write out the header WriteHeader(sw, datdata, logger); // Write out each of the machines and roms int depth = 2, last = -1; string lastgame = null; List splitpath = new List(); foreach (List roms in sortable.Values) { for (int index = 0; index < roms.Count; index++) { Rom rom = roms[index]; List newsplit = rom.Machine.Name.Split('\\').ToList(); // If we have a different game and we're not at the start of the list, output the end of last item if (lastgame != null && lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) { depth = WriteEndGame(sw, rom, splitpath, newsplit, lastgame, datdata, depth, out last, logger); } // If we have a new game, output the beginning of the new item if (lastgame == null || lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) { depth = WriteStartGame(sw, rom, newsplit, lastgame, datdata, depth, last, logger); } // If we have a "null" game (created by DATFromDir or something similar), log it to file if (rom.Name == "null" && rom.HashData.Size == -1 && rom.HashData.CRC == "null" && rom.HashData.MD5 == "null" && rom.HashData.SHA1 == "null") { logger.Log("Empty folder found: " + rom.Machine); // If we're in a mode that doesn't allow for actual empty folders, add the blank info if (datdata.OutputFormat != OutputFormat.SabreDat && datdata.OutputFormat != OutputFormat.MissFile) { rom.Name = "-"; rom.HashData.Size = Constants.SizeZero; rom.HashData.CRC = Constants.CRCZero; rom.HashData.MD5 = Constants.MD5Zero; rom.HashData.SHA1 = Constants.SHA1Zero; } // Otherwise, set the new path and such, write out, and continue else { splitpath = newsplit; lastgame = rom.Machine.Name; continue; } } // Now, output the rom data WriteRomData(sw, rom, lastgame, datdata, depth, logger); // Set the new data to compare against splitpath = newsplit; lastgame = rom.Machine.Name; } } // Write the file footer out WriteFooter(sw, datdata, depth, logger); logger.Log("File written!" + Environment.NewLine); sw.Close(); fs.Close(); } catch (Exception ex) { logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DAT header using the supplied StreamWriter /// /// StreamWriter to output to /// DatData object representing DAT information /// Logger object for file and console output /// True if the data was written, false on error public static bool WriteHeader(StreamWriter sw, Dat datdata, Logger logger) { try { string header = ""; switch (datdata.OutputFormat) { case OutputFormat.ClrMamePro: header = "clrmamepro (\n" + "\tname \"" + datdata.Name + "\"\n" + "\tdescription \"" + datdata.Description + "\"\n" + "\tcategory \"" + datdata.Category + "\"\n" + "\tversion \"" + datdata.Version + "\"\n" + "\tdate \"" + datdata.Date + "\"\n" + "\tauthor \"" + datdata.Author + "\"\n" + "\temail \"" + datdata.Email + "\"\n" + "\thomepage \"" + datdata.Homepage + "\"\n" + "\turl \"" + datdata.Url + "\"\n" + "\tcomment \"" + datdata.Comment + "\"\n" + (datdata.ForcePacking == ForcePacking.Unzip ? "\tforcezipping no\n" : "") + ")\n"; break; case OutputFormat.MissFile: if (datdata.XSV == true) { header = "\"File Name\"\t\"Internal Name\"\t\"Description\"\t\"Game Name\"\t\"Game Description\"\t\"Type\"\t\"" + "Rom Name\"\t\"Disk Name\"\t\"Size\"\t\"CRC\"\t\"MD5\"\t\"SHA1\"\t\"Nodump\"\n"; } else if (datdata.XSV == false) { header = "\"File Name\",\"Internal Name\",\"Description\",\"Game Name\",\"Game Description\",\"Type\",\"" + "Rom Name\",\"Disk Name\",\"Size\",\"CRC\",\"MD5\",\"SHA1\",\"Nodump\"\n"; } break; case OutputFormat.RomCenter: header = "[CREDITS]\n" + "author=" + datdata.Author + "\n" + "version=" + datdata.Version + "\n" + "comment=" + datdata.Comment + "\n" + "[DAT]\n" + "version=2.50\n" + "split=" + (datdata.ForceMerging == ForceMerging.Split ? "1" : "0") + "\n" + "merge=" + (datdata.ForceMerging == ForceMerging.Full ? "1" : "0") + "\n" + "[EMULATOR]\n" + "refname=" + datdata.Name + "\n" + "version=" + datdata.Description + "\n" + "[GAMES]\n"; break; case OutputFormat.SabreDat: header = "\n" + "\n\n" + "\n" + "\t
\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Name) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Description) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.RootDir) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Category) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Version) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Date) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Author) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Comment) + "\n" + (!String.IsNullOrEmpty(datdata.Type) && datdata.ForcePacking != ForcePacking.Unzip ? "\t\t\n" + (!String.IsNullOrEmpty(datdata.Type) ? "\t\t\t\n" : "") + (datdata.ForcePacking == ForcePacking.Unzip ? "\t\t\t\n" : "") + "\t\t\n" : "") + "\t
\n" + "\t\n"; break; case OutputFormat.Xml: header = "\n" + "\n\n" + "\n" + "\t
\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Name) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Description) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.RootDir) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Category) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Version) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Date) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Author) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Email) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Homepage) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Url) + "\n" + "\t\t" + HttpUtility.HtmlEncode(datdata.Comment) + "\n" + (!String.IsNullOrEmpty(datdata.Type) ? "\t\t" + datdata.Type + "\n" : "") + (datdata.ForcePacking == ForcePacking.Unzip ? "\t\t\n" : "") + "\t
\n"; break; } // Write the header out sw.Write(header); } catch (Exception ex) { logger.Error(ex.ToString()); return false; } return true; } /// /// Write out Game start using the supplied StreamWriter /// /// StreamWriter to output to /// RomData object to be output /// Split path representing the parent game (SabreDAT only) /// The name of the last game to be output /// DatData object representing DAT information /// Current depth to output file at (SabreDAT only) /// Last known depth to cycle back from (SabreDAT only) /// Logger object for file and console output /// The new depth of the tag public static int WriteStartGame(StreamWriter sw, Rom rom, List newsplit, string lastgame, Dat datdata, int depth, int last, Logger logger) { try { // No game should start with a path separator if (rom.Machine.Name.StartsWith(Path.DirectorySeparatorChar.ToString())) { rom.Machine.Name = rom.Machine.Name.Substring(1); } string state = ""; switch (datdata.OutputFormat) { case OutputFormat.ClrMamePro: state += "game (\n\tname \"" + rom.Machine + "\"\n" + "\tdescription \"" + (String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description) + "\"\n"; break; case OutputFormat.SabreDat: for (int i = (last == -1 ? 0 : last); i < newsplit.Count; i++) { for (int j = 0; j < depth - last + i - (lastgame == null ? 1 : 0); j++) { state += "\t"; } state += "\n"; } depth = depth - (last == -1 ? 0 : last) + newsplit.Count; break; case OutputFormat.Xml: state += "\t\n" + "\t\t" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + "\n"; break; } sw.Write(state); } catch (Exception ex) { logger.Error(ex.ToString()); return depth; } return depth; } /// /// Write out Game start using the supplied StreamWriter /// /// StreamWriter to output to /// RomData object to be output /// Split path representing last kwown parent game (SabreDAT only) /// Split path representing the parent game (SabreDAT only) /// The name of the last game to be output /// DatData object representing DAT information /// Current depth to output file at (SabreDAT only) /// Last known depth to cycle back from (SabreDAT only) /// Logger object for file and console output /// The new depth of the tag public static int WriteEndGame(StreamWriter sw, Rom rom, List splitpath, List newsplit, string lastgame, Dat datdata, int depth, out int last, Logger logger) { last = 0; try { string state = ""; switch (datdata.OutputFormat) { case OutputFormat.ClrMamePro: state += ")\n"; break; case OutputFormat.SabreDat: if (splitpath != null) { for (int i = 0; i < newsplit.Count && i < splitpath.Count; i++) { // Always keep track of the last seen item last = i; // If we find a difference, break if (newsplit[i] != splitpath[i]) { break; } } // Now that we have the last known position, take down all open folders for (int i = depth - 1; i > last + 1; i--) { // Print out the number of tabs and the end folder for (int j = 0; j < i; j++) { state += "\t"; } state += "\n"; } // Reset the current depth depth = 2 + last; } break; case OutputFormat.Xml: state += "\t\n"; break; } sw.Write(state); } catch (Exception ex) { logger.Error(ex.ToString()); return depth; } return depth; } /// /// Write out RomData using the supplied StreamWriter /// /// StreamWriter to output to /// RomData object to be output /// The name of the last game to be output /// DatData object representing DAT information /// Current depth to output file at (SabreDAT only) /// Logger object for file and console output /// True if the data was written, false on error public static bool WriteRomData(StreamWriter sw, Rom rom, string lastgame, Dat datdata, int depth, Logger logger) { try { string state = ""; switch (datdata.OutputFormat) { case OutputFormat.ClrMamePro: state += "\t" + rom.Type.ToString().ToLowerInvariant() + " ( name \"" + rom.Name + "\"" + (rom.HashData.Size != -1 ? " size " + rom.HashData.Size : "") + (!String.IsNullOrEmpty(rom.HashData.CRC) ? " crc " + rom.HashData.CRC.ToLowerInvariant() : "") + (!String.IsNullOrEmpty(rom.HashData.MD5) ? " md5 " + rom.HashData.MD5.ToLowerInvariant() : "") + (!String.IsNullOrEmpty(rom.HashData.SHA1) ? " sha1 " + rom.HashData.SHA1.ToLowerInvariant() : "") + (!String.IsNullOrEmpty(rom.Date) ? " date \"" + rom.Date + "\"" : "") + (rom.Nodump ? " flags nodump" : "") + " )\n"; break; case OutputFormat.MissFile: string pre = datdata.Prefix + (datdata.Quotes ? "\"" : ""); string post = (datdata.Quotes ? "\"" : "") + datdata.Postfix; // Check for special strings in prefix and postfix pre = pre.Replace("%crc%", rom.HashData.CRC).Replace("%md5%", rom.HashData.MD5).Replace("%sha1%", rom.HashData.SHA1).Replace("%size%", rom.HashData.Size.ToString()); post = post.Replace("%crc%", rom.HashData.CRC).Replace("%md5%", rom.HashData.MD5).Replace("%sha1%", rom.HashData.SHA1).Replace("%size%", rom.HashData.Size.ToString()); // If we're in Romba mode, the state is consistent if (datdata.Romba) { // We can only write out if there's a SHA-1 if (rom.HashData.SHA1 != "") { string name = rom.HashData.SHA1.Substring(0, 2) + "/" + rom.HashData.SHA1.Substring(2, 2) + "/" + rom.HashData.SHA1.Substring(4, 2) + "/" + rom.HashData.SHA1.Substring(6, 2) + "/" + rom.HashData.SHA1 + ".gz"; state += pre + name + post + "\n"; } } // If we're in TSV mode, similarly the state is consistent else if (datdata.XSV == true) { string inline = "\"" + datdata.FileName + "\"\t\"" + datdata.Name + "\"\t\"" + datdata.Description + "\"\t\"" + rom.Machine + "\"\t\"" + rom.Machine + "\"\t\"" + rom.Type.ToString().ToLowerInvariant() + "\"\t\"" + (rom.Type == ItemType.Rom ? rom.Name : "") + "\"\t\"" + (rom.Type == ItemType.Disk ? rom.Name : "") + "\"\t\"" + rom.HashData.Size + "\"\t\"" + rom.HashData.CRC + "\"\t\"" + rom.HashData.MD5 + "\"\t\"" + rom.HashData.SHA1 + "\"\t" + (rom.Nodump ? "\"Nodump\"" : "\"\""); state += pre + inline + post + "\n"; } // If we're in CSV mode, similarly the state is consistent else if (datdata.XSV == false) { string inline = "\"" + datdata.FileName + "\",\"" + datdata.Name + "\",\"" + datdata.Description + "\",\"" + rom.Machine + "\",\"" + rom.Machine + "\",\"" + rom.Type.ToString().ToLowerInvariant() + "\",\"" + (rom.Type == ItemType.Rom ? rom.Name : "") + "\",\"" + (rom.Type == ItemType.Disk ? rom.Name : "") + "\",\"" + rom.HashData.Size + "\",\"" + rom.HashData.CRC + "\",\"" + rom.HashData.MD5 + "\",\"" + rom.HashData.SHA1 + "\"," + (rom.Nodump ? "\"Nodump\"" : "\"\""); state += pre + inline + post + "\n"; } // Otherwise, use any flags else { string name = (datdata.UseGame ? rom.Machine.Name : rom.Name); if (datdata.RepExt != "") { string dir = Path.GetDirectoryName(name); dir = (dir.StartsWith(Path.DirectorySeparatorChar.ToString()) ? dir.Remove(0, 1) : dir); name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + datdata.RepExt); } if (datdata.AddExt != "") { name += datdata.AddExt; } if (!datdata.UseGame && datdata.GameName) { name = Path.Combine(rom.Machine.Name, name); } if (datdata.UseGame && rom.Machine.Name != lastgame) { state += pre + name + post + "\n"; lastgame = rom.Machine.Name; } else if (!datdata.UseGame) { state += pre + name + post + "\n"; } } break; case OutputFormat.RomCenter: state += "¬¬¬" + HttpUtility.HtmlEncode(rom.Machine) + "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + "¬" + HttpUtility.HtmlEncode(rom.Name) + "¬" + rom.HashData.CRC.ToLowerInvariant() + "¬" + (rom.HashData.Size != -1 ? rom.HashData.Size.ToString() : "") + "¬¬¬\n"; break; case OutputFormat.SabreDat: string prefix = ""; for (int i = 0; i < depth; i++) { prefix += "\t"; } state += prefix; state += "\n" + prefix + "\t\n" + prefix + "\t\t\n" + prefix + "\t\n" + prefix + "\n" : "/>\n"); break; case OutputFormat.Xml: state += "\t\t<" + rom.Type.ToString().ToLowerInvariant() + " name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + (rom.HashData.Size != -1 ? " size=\"" + rom.HashData.Size + "\"" : "") + (!String.IsNullOrEmpty(rom.HashData.CRC) ? " crc=\"" + rom.HashData.CRC.ToLowerInvariant() + "\"" : "") + (!String.IsNullOrEmpty(rom.HashData.MD5) ? " md5=\"" + rom.HashData.MD5.ToLowerInvariant() + "\"" : "") + (!String.IsNullOrEmpty(rom.HashData.SHA1) ? " sha1=\"" + rom.HashData.SHA1.ToLowerInvariant() + "\"" : "") + (!String.IsNullOrEmpty(rom.Date) ? " date=\"" + rom.Date + "\"" : "") + (rom.Nodump ? " status=\"nodump\"" : "") + "/>\n"; break; } sw.Write(state); } catch (Exception ex) { logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DAT footer using the supplied StreamWriter /// /// StreamWriter to output to /// DatData object representing DAT information /// /// Current depth to output file at (SabreDAT only) /// Logger object for file and console output /// True if the data was written, false on error public static bool WriteFooter(StreamWriter sw, Dat datdata, int depth, Logger logger) { try { string footer = ""; // If we have roms, output the full footer if (datdata.Files != null && datdata.Files.Count > 0) { switch (datdata.OutputFormat) { case OutputFormat.ClrMamePro: footer = ")"; break; case OutputFormat.SabreDat: for (int i = depth - 1; i >= 2; i--) { // Print out the number of tabs and the end folder for (int j = 0; j < i; j++) { footer += "\t"; } footer += "\n"; } footer += "\t
\n
"; break; case OutputFormat.Xml: footer = "\t\n"; break; } } // Otherwise, output the abbreviated form else { switch (datdata.OutputFormat) { case OutputFormat.SabreDat: case OutputFormat.Xml: footer = ""; break; } } // Write the footer out sw.Write(footer); } catch (Exception ex) { logger.Error(ex.ToString()); return false; } return true; } /// /// Cleans out the temporary directory /// /// Name of the directory to clean out public static void CleanDirectory(string dirname) { foreach (string file in Directory.EnumerateFiles(dirname, "*", SearchOption.TopDirectoryOnly)) { try { File.Delete(file); } catch { } } foreach (string dir in Directory.EnumerateDirectories(dirname, "*", SearchOption.TopDirectoryOnly)) { try { Directory.Delete(dir, true); } catch { } } } /// /// Remove an arbitrary number of bytes from the inputted file /// /// File to be cropped /// Outputted file /// Bytes to be removed from head of file /// Bytes to be removed from tail of file public static void RemoveBytesFromFile(string input, string output, long bytesToRemoveFromHead, long bytesToRemoveFromTail) { // If any of the inputs are invalid, skip if (!File.Exists(input) || new FileInfo(input).Length <= (bytesToRemoveFromHead + bytesToRemoveFromTail)) { return; } // Read the input file and write to the fail using (BinaryReader br = new BinaryReader(File.OpenRead(input))) using (BinaryWriter bw = new BinaryWriter(File.OpenWrite(output))) { int bufferSize = 1024; long adjustedLength = br.BaseStream.Length - bytesToRemoveFromTail; // Seek to the correct position br.BaseStream.Seek((bytesToRemoveFromHead < 0 ? 0 : bytesToRemoveFromHead), SeekOrigin.Begin); // Now read the file in chunks and write out byte[] buffer = new byte[bufferSize]; while (br.BaseStream.Position <= (adjustedLength - bufferSize)) { buffer = br.ReadBytes(bufferSize); bw.Write(buffer); } // For the final chunk, if any, write out only that number of bytes int length = (int)(adjustedLength - br.BaseStream.Position); buffer = new byte[length]; buffer = br.ReadBytes(length); bw.Write(buffer); } } /// /// Add an aribtrary number of bytes to the inputted file /// /// File to be appended to /// Outputted file /// String representing bytes to be added to head of file /// String representing bytes to be added to tail of file public static void AppendBytesToFile(string input, string output, string bytesToAddToHead, string bytesToAddToTail) { // Source: http://stackoverflow.com/questions/311165/how-do-you-convert-byte-array-to-hexadecimal-string-and-vice-versa byte[] bytesToAddToHeadArray = new byte[bytesToAddToHead.Length / 2]; for (int i = 0; i < bytesToAddToHead.Length; i += 2) { bytesToAddToHeadArray[i/2] = Convert.ToByte(bytesToAddToHead.Substring(i, 2), 16); } byte[] bytesToAddToTailArray = new byte[bytesToAddToTail.Length / 2]; for (int i = 0; i < bytesToAddToTail.Length; i += 2) { bytesToAddToTailArray[i / 2] = Convert.ToByte(bytesToAddToTail.Substring(i, 2), 16); } AppendBytesToFile(input, output, bytesToAddToHeadArray, bytesToAddToTailArray); } /// /// Add an aribtrary number of bytes to the inputted file /// /// File to be appended to /// Outputted file /// Bytes to be added to head of file /// Bytes to be added to tail of file public static void AppendBytesToFile(string input, string output, byte[] bytesToAddToHead, byte[] bytesToAddToTail) { // If any of the inputs are invalid, skip if (!File.Exists(input)) { return; } using (BinaryReader br = new BinaryReader(File.OpenRead(input))) using (BinaryWriter bw = new BinaryWriter(File.OpenWrite(output))) { if (bytesToAddToHead.Count() > 0) { bw.Write(bytesToAddToHead); } int bufferSize = 1024; // Now read the file in chunks and write out byte[] buffer = new byte[bufferSize]; while (br.BaseStream.Position <= (br.BaseStream.Length - bufferSize)) { buffer = br.ReadBytes(bufferSize); bw.Write(buffer); } // For the final chunk, if any, write out only that number of bytes int length = (int)(br.BaseStream.Length - br.BaseStream.Position); buffer = new byte[length]; buffer = br.ReadBytes(length); bw.Write(buffer); if (bytesToAddToTail.Count() > 0) { bw.Write(bytesToAddToTail); } } } /// /// Copy a file to a new location, creating directories as needed /// /// Input filename /// Output filename public static void CopyFileToNewLocation(string input, string output) { if (File.Exists(input) && !File.Exists(output)) { if (!Directory.Exists(Path.GetDirectoryName(output))) { Directory.CreateDirectory(Path.GetDirectoryName(output)); } File.Copy(input, output); } } } }