/* TODO: * private void WriteDataInline(Stream stream, List<Entry> entries, Dictionary<Entry, Structs.RPF7EntryInfoTemplate> entriesInfo, int headersEnd) * { * Range<FileEntry> oldFileLayout = new Range<FileEntry>(Math.Max(stream.Length, (long)headersEnd)); * List<FileEntry> emptyEntries = new List<FileEntry>(); * List<FileEntry> moveEntries = new List<FileEntry>(); * List<FileEntry> newEntries = new List<FileEntry>(); * // Entries that should be written to temporary files * List<FileEntry> newSavedEntries = new List<FileEntry>(); * * // A dictionary of the raw data stream that we need to write to the file * Dictionary<FileEntry, Stream> entriesStream = new Dictionary<FileEntry, System.IO.Stream>(); * * // Add the header * oldFileLayout.AddItem(0, headersEnd, null); * * foreach (Entry entry in entries) * { * FileEntry fentry = entry as FileEntry; * if (fentry != null) * { * * // We need to find all the files that depends on the origianl stream first * FileStreamCreator originalStream = fentry.TryGetOriginalFileStreamCreator(); * if (fentry.Data.GetSize() == 0 && fentry.IsRegularFile() && !((RegularFileEntry)fentry).Compressed) * { * emptyEntries.Add(fentry); * } * else if (originalStream != null && originalStream.FileStream == this.Stream) * { * if (originalStream.Offset < headersEnd) { * moveEntries.Add(fentry); * } * if (!oldFileLayout.AddItem(originalStream.Offset, originalStream.Size, fentry, false)) * { * // If failed for some reason.. well it can happen if two entries have the same data. Anyway I don't want that two entries will use the same data. * newSavedEntries.Add(fentry); * } * else * { * entriesStream[fentry] = new PartialStream(this.Stream, originalStream.Offset, originalStream.Size); * } * } * else if (fentry.Data is FileStreamCreator && ((FileStreamCreator)fentry.Data).FileStream == this.Stream) * { * // This file points to the original file, but something about it changed (compression, encryption, ..) * newSavedEntries.Add(fentry); * } * // From here are entries that are not depends on the original strema * else if ((fentry.IsRegularFile() && ((RegularFileEntry)fentry).Compressed) || fentry.IsResource()) * { * newSavedEntries.Add(fentry); * } * else * { * entriesStream[fentry] = fentry.Data.GetStream(); * newEntries.Add(fentry); * } * } * } * * foreach (FileEntry entry in newSavedEntries) * { * string filepath = Path.GetTempFileName(); * // Let's copy the file to temp folder, this file will be deleted on close * FileStream writeStream = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 0x1000, FileOptions.DeleteOnClose); * * entry.Data.GetStream().CopyTo(writeStream); * * // Take an handle to the file, so it will be delete only when this handle will be closed. * FileStream readStream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); * writeStream.Close(); * * entriesStream[entry] = writeStream; * } * * Range<FileEntry> newFileLayout = new Range<FileEntry>(); * } */ public void Write(Stream stream, IProgressReport progressReport = null) { if (progressReport != null) { progressReport.SetMessage("Preparing..."); progressReport.SetProgress(-1); } if (this.Filename == "") { throw new Exception("Can't save"); } // Get all the entries List <Entry> entries = new List <Entry>(); this.Root.AddToList(entries); Dictionary <Entry, Structs.RPF7EntryInfoTemplate> entriesInfo = new Dictionary <Entry, Structs.RPF7EntryInfoTemplate>(); foreach (Entry entry in entries) { Structs.RPF7EntryInfoTemplate entryInfo = new Structs.RPF7EntryInfoTemplate(); // update the is resource field entryInfo.Field1 = (entry is ResourceEntry) ? 1U : 0U; entriesInfo[entry] = entryInfo; } int shiftNameAccessBy; Stream names = WriteNames(entries, entriesInfo, out shiftNameAccessBy); // Fill the header Info.EntriesCount = entries.Count; Info.ShiftNameAccessBy = shiftNameAccessBy; Info.EntriesNamesLength = (int)names.Length; // Go to current position stream.Seek(0x10 + 0x10 * Info.EntriesCount + Info.EntriesNamesLength, SeekOrigin.Begin); // align to 0x200 if (stream.Position % (1 << 9) != 0) { stream.Write(new byte[(1 << 9) - ((int)stream.Position % (1 << 9))], 0, (1 << 9) - ((int)stream.Position % (1 << 9))); } WriteData(stream, entries, entriesInfo, progressReport); long end = stream.Position; using (Stream writer = AES.EncryptStream(new StreamKeeper(stream), sixteenRoundsDecrypt)) { int currentFreeIndex = 1; // Last finish to build and write the file stream.Seek(0, SeekOrigin.Begin); Info.Write(stream); Queue <Entry> entriesToProcess = new Queue <Entry>(); entriesToProcess.Enqueue(this.Root); while (entriesToProcess.Count > 0) { Entry entry = entriesToProcess.Dequeue(); if (entry is DirectoryEntry) { Structs.RPF7EntryInfoTemplate entryInfo = entriesInfo[entry]; IList <Entry> subentries = (entry as DirectoryEntry).GetEntries(); // Update the info on the sub entries entryInfo.Field5 = (uint)currentFreeIndex; entryInfo.Field6 = (uint)subentries.Count; entriesInfo[entry] = entryInfo; // Process the sub entries foreach (Entry subentry in subentries) { entriesToProcess.Enqueue(subentry); } currentFreeIndex += subentries.Count; } // Write the entry info entriesInfo[entry].Write(writer); } // Write names names.Seek(0, SeekOrigin.Begin); names.CopyTo(writer); } // Seek to end stream.Seek(end, SeekOrigin.Begin); // Done! }
private MemoryStream WriteNames(List <Entry> entries, Dictionary <Entry, Structs.RPF7EntryInfoTemplate> entriesInfo, out int shiftNameAccessBy) { IEnumerable <string> entriesNames = entries.Select(entry => entry.Name); int namesLength = entriesNames.Sum(name => name.Length); shiftNameAccessBy = -1; for (int i = 0; i < 8; ++i) { // the multipcation is the maximum number of nulls, TODO: it can be done better, because it is not always the worst case // does the offset should be signed or unsigned? if (namesLength + entriesNames.Count() * (i + 1) < (1 << (16 + i))) { shiftNameAccessBy = i; break; } } if (shiftNameAccessBy == -1) { throw new Exception("Too many entries!"); } int currentPos = 0; MemoryStream filenames = new MemoryStream(); SortedList <string, int> WritedNames = new SortedList <string, int>(); using (BinaryWriter writer = new BinaryWriter(new StreamKeeper(filenames))) { foreach (Entry entry in entries) { int nameOffset; // Check if we already wrote this name, if it is use it if (!WritedNames.TryGetValue(entry.Name, out nameOffset)) { nameOffset = currentPos; WritedNames[entry.Name] = nameOffset; // write name and null, and keep the position writer.Write(entry.Name.ToArray <char>()); writer.Write((byte)0); currentPos += entry.Name.Length + 1; // Make the next entry alligned while ((currentPos % (1 << shiftNameAccessBy)) != 0) { writer.Write((byte)0); currentPos += 1; } } // Update name offset in entry info Structs.RPF7EntryInfoTemplate entryInfo = entriesInfo[entry]; entryInfo.Field4 = (uint)nameOffset; entriesInfo[entry] = entryInfo; } // Align to 0x10 byte while (currentPos % 0x10 != 0) { writer.Write((byte)0); currentPos += 1; } } return(filenames); }
private void WriteData(Stream stream, List <Entry> entries, Dictionary <Entry, Structs.RPF7EntryInfoTemplate> entriesInfo, IProgressReport progressReport = null) { if (progressReport != null) { progressReport = new SubProgressReport(progressReport, entries.Sum(entry => (entry is FileEntry ? ((FileEntry)entry).Data.GetSize() : 0L))); } long passed = 0; // Write data and fill entry info foreach (Entry entry in entries) { if (progressReport != null && progressReport.IsCanceled()) { throw new OperationCanceledException(); } if (entry is FileEntry) { if (progressReport != null) { progressReport.SetMessage(String.Format("Writing file {0}.", entry.Name)); } long entryOffset = stream.Position; (entry as FileEntry).Write(stream); int entrySize = (int)(stream.Position - entryOffset); // align to 0x200 if (stream.Position % (1 << 9) != 0) { stream.Write(new byte[(1 << 9) - ((int)stream.Position % (1 << 9))], 0, (1 << 9) - ((int)stream.Position % (1 << 9))); } // Update the entry offset and entry size Structs.RPF7EntryInfoTemplate entryInfo = entriesInfo[entry]; entryInfo.Field2 = (uint)((entryOffset >> 9) & 0x7FFFFF); if (entry is ResourceEntry) { entryInfo.Field3 = (uint)entrySize & 0xFFFFFF; entryInfo.Field5 = (entry as ResourceEntry).SystemFlag; entryInfo.Field6 = (entry as ResourceEntry).GraphicsFlag; } else if (entry is RegularFileEntry) { if ((entry as RegularFileEntry).Compressed) { // The compressed size entryInfo.Field3 = (uint)entrySize & 0xFFFFFF; // The uncompress size entryInfo.Field5 = (uint)(entry as RegularFileEntry).Data.GetSize(); // The is encrypted flag entryInfo.Field6 = 1; } else { entryInfo.Field3 = 0; entryInfo.Field5 = (uint)entrySize; // The is encrypted flag entryInfo.Field6 = 0; } } entriesInfo[entry] = entryInfo; if (progressReport != null) { passed += (entry as FileEntry).Data.GetSize(); progressReport.SetProgress(passed); } } else { Structs.RPF7EntryInfoTemplate entryInfo = entriesInfo[entry]; // The offset is "-1" in case of file entryInfo.Field2 = 0x7FFFFF; entriesInfo[entry] = entryInfo; } } }
private Entry CreateFromHeader(Structs.RPF7EntryInfoTemplate entryInfo, MemoryStream entriesInfo, MemoryStream filenames, Range <Boolean> fileUsage) { // Todo: check sizes and offsets bool isResource = entryInfo.Field1 == 1; long offset = (long)entryInfo.Field2; int compressedSize = (int)entryInfo.Field3; int filenameOffset = (int)entryInfo.Field4; filenameOffset <<= Info.ShiftNameAccessBy; if (filenameOffset < 0 || filenameOffset >= filenames.Length) { throw new RPFEntryParsingException("Reading entry name: Invalid offst"); } filenames.Seek(filenameOffset, SeekOrigin.Begin); String filename = ""; // Read null-terminated filename int currentChar; while ((currentChar = filenames.ReadByte()) != 0) { if (currentChar == -1) { throw new RPFEntryParsingException("Reading entry name: Unexpected EOF"); } filename += (char)currentChar; } // There may be duplicate names usage, so don't check it if (!fileUsage.AddItem(0x10 + entriesInfo.Length + filenameOffset, filename.Length + 1, true)) { throw new RPFEntryParsingException("Reading entry entry: Invalid duplicate usage of a name"); } if (offset == 0x7FFFFF) { // Is a Directory if (isResource) { throw new RPFEntryParsingException("Invalid entry type (directory and resource)"); } int subentriesStartIndex = (int)entryInfo.Field5; int subentriesCount = (int)entryInfo.Field6; if (subentriesStartIndex < 0 || (subentriesStartIndex + subentriesCount) * 0x10 > entriesInfo.Length) { throw new RPFEntryParsingException("Invalid directory entry: Invalid subentries info"); } List <Entry> entries = new List <Entry>(); // This will prevent recursion.. if (!fileUsage.AddItem(0x10 * (1 + subentriesStartIndex), 0x10 * subentriesCount, true, false)) { throw new RPFEntryParsingException("Reading directory entry: Duplicate usage of an entries - may lead to recursion"); } for (int i = 0; i < subentriesCount; ++i) { entriesInfo.Seek(0x10 * (i + subentriesStartIndex), SeekOrigin.Begin); entries.Add(CreateFromHeader(new Structs.RPF7EntryInfoTemplate(entriesInfo), entriesInfo, filenames, fileUsage)); } return(new DirectoryEntry(filename, entries)); } offset <<= 9; if (offset < 0 || offset > Stream.Length) { throw new RPFEntryParsingException("Invalid entry info: Invalid data offset"); } if (isResource) { if (compressedSize == 0xFFFFFF) { throw new RPFEntryParsingException("Resource with size -1, not supported (Contact developr if seen)"); } if (!fileUsage.AddItem(offset, compressedSize, true, false)) // Can't use the same data for two files { throw new RPFEntryParsingException("Reading entry data: Data position is wierd"); } uint systemFlag = entryInfo.Field5; uint graphicsFlag = entryInfo.Field6; return(new ResourceEntry(filename, new ResourceStreamCreator(Stream, offset, compressedSize, systemFlag, graphicsFlag, Path.GetExtension(filename).Substring(2)), systemFlag, graphicsFlag)); } // Regular file int uncompressedSize = (int)entryInfo.Field5; int isEncrypted = (int)entryInfo.Field6; if (compressedSize == 0) { // Uncompressed file if (isEncrypted != 0) { throw new RPFEntryParsingException("Unexcepted file - compressed but unencrypted (Contact developr if seen)"); } if (!fileUsage.AddItem(offset, uncompressedSize, true, false)) // Can't use the same data for two files { throw new RPFEntryParsingException("Reading entry data: Data position is wierd"); } return(new RegularFileEntry(filename, new FileStreamCreator(Stream, offset, uncompressedSize), false)); } else { if (!fileUsage.AddItem(offset, compressedSize, true, false)) // Can't use the same data for two files { throw new RPFEntryParsingException("Reading entry data: Data position is wierd"); } // Compressed file return(new RegularFileEntry(filename, new CompressedFileStreamCreator(Stream, offset, compressedSize, uncompressedSize, isEncrypted != 0), true)); } }