/// <summary> /// Allocates a new tag at the end of the tag list without updating the file. /// You can give the tag data by using one of the overwrite functions. /// </summary> /// <param name="type">The tag's type information.</param> /// <returns>The allocated tag.</returns> public TagInstance AllocateTag(TagGroup type) { var tagIndex = _tags.Count; var tag = new TagInstance(tagIndex, type); _tags.Add(tag); return tag; }
/// <summary> /// Fixes tag offsets after a resize operation. /// </summary> /// <param name="startOffset">The offset where the resize operation took place.</param> /// <param name="sizeDelta">The amount to add to each tag offset after the start offset.</param> /// <param name="ignore">A tag to ignore.</param> private void FixTagOffsets(long startOffset, long sizeDelta, TagInstance ignore) { foreach (var adjustTag in _tags.Where(t => t != null && t != ignore && t.HeaderOffset >= startOffset)) { adjustTag.HeaderOffset += sizeDelta; } }
/// <summary> /// Overwrites a tag's data, not including its header. /// Any pointers in the data to write will be adjusted to be relative to the start of the tag's header. /// Make sure that fixups are set correctly before calling this. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to write.</param> public void OverwriteTagData(Stream stream, TagInstance tag, byte[] data) { if (tag == null) { throw new ArgumentNullException("tag"); } // Ensure the data fits var newHeaderSize = CalculateNewHeaderSize(tag); if (tag.HeaderOffset < 0) { tag.HeaderOffset = GetNewTagOffset(tag.Index); } var alignedLength = (data.Length + 0xF) & ~0xF; ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, newHeaderSize + alignedLength); tag.DataOffset = tag.HeaderOffset + newHeaderSize; tag.DataSize = alignedLength; // Write it in and then update stream.Position = tag.DataOffset; stream.Write(data, 0, data.Length); StreamUtil.Fill(stream, 0, alignedLength - data.Length); UpdateTag(stream, tag); }
/// <summary> /// Reads a tag's data from the file. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <returns>The data that was read.</returns> public TagData ExtractTag(Stream stream, TagInstance tag) { if (tag == null) { throw new ArgumentNullException(nameof(tag)); } if (tag.HeaderOffset < 0) { throw new ArgumentException("The tag is not in the cache file"); } // Build the description info and get the data offset uint dataOffset; var data = BuildTagDescription(stream, tag, out dataOffset); // Read the tag data stream.Position = tag.HeaderOffset + dataOffset; data.Data = new byte[tag.TotalSize - dataOffset]; stream.Read(data.Data, 0, data.Data.Length); // Correct pointers using (var dataWriter = new BinaryWriter(new MemoryStream(data.Data))) { foreach (var fixup in data.PointerFixups) { dataWriter.BaseStream.Position = fixup.WriteOffset; dataWriter.Write(tag.OffsetToPointer(fixup.TargetOffset)); } } return(data); }
/// <summary> /// Allocates a new tag at the end of the tag list without updating the file. /// You can give the tag data by using one of the overwrite functions. /// </summary> /// <param name="type">The tag's type information.</param> /// <returns>The allocated tag.</returns> public TagInstance AllocateTag(TagGroup type) { var tagIndex = _tags.Count; var tag = new TagInstance(tagIndex, type); _tags.Add(tag); return(tag); }
/// <summary> /// Updates a tag's fixup information. /// </summary> /// <param name="writer">The stream to write to.</param> /// <param name="tag">The tag.</param> private static void UpdateTagFixups(BinaryWriter writer, TagInstance tag) { var totalHeaderSize = CalculateNewHeaderSize(tag); foreach (var fixup in tag.DataFixups) { writer.BaseStream.Position = tag.DataOffset + fixup.WriteOffset; writer.Write(fixup.TargetOffset + totalHeaderSize + FixupPointerBase); } }
/// <summary> /// Allocates a new tag at the end of the tag list without updating the file. /// You can give the tag data by using one of the overwrite functions. /// </summary> /// <returns>The allocated tag.</returns> public TagInstance AllocateTag() { var tagIndex = _tags.Count; var tag = new TagInstance { Index = tagIndex }; _tags.Add(tag); return(tag); }
/// <summary> /// Rebases fixup pointers in tag data. /// </summary> /// <param name="tag">The tag.</param> /// <param name="data">The data.</param> /// <param name="newBase">The new base offset to use.</param> private static void RebasePointers(TagInstance tag, byte[] data, uint newBase) { using (var writer = new BinaryWriter(new MemoryStream(data))) { foreach (var fixup in tag.DataFixups) { writer.BaseStream.Position = fixup.WriteOffset; writer.Write(newBase + fixup.TargetOffset + FixupPointerBase); } } }
/// <summary> /// Reads a tag's raw data from the file, including its header. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <returns>The data that was read.</returns> public byte[] ExtractTagRaw(Stream stream, TagInstance tag) { if (tag == null) throw new ArgumentNullException(nameof(tag)); if (tag.HeaderOffset < 0) throw new ArgumentException("The tag is not in the cache file"); stream.Position = tag.HeaderOffset; var result = new byte[tag.TotalSize]; stream.Read(result, 0, result.Length); return result; }
/// <summary> /// Resizes a block of data in the file and updates tag offsets. /// </summary> /// <param name="stream">The stream.</param> /// <param name="tag">The tag that the block belongs to, if any.</param> /// <param name="startOffset">The offset where the block to resize begins at.</param> /// <param name="oldSize">The current size of the block to resize.</param> /// <param name="newSize">The new size of the block.</param> /// <exception cref="System.ArgumentException">Cannot resize a block to a negative size</exception> private void ResizeBlock(Stream stream, TagInstance tag, long startOffset, long oldSize, long newSize) { if (newSize < 0) { throw new ArgumentException("Cannot resize a block to a negative size"); } var oldEndOffset = startOffset + oldSize; var sizeDelta = newSize - oldSize; StreamUtil.Copy(stream, oldEndOffset, oldEndOffset + sizeDelta, stream.Length - oldEndOffset); FixTagOffsets(oldEndOffset, sizeDelta, tag); }
/// <summary> /// Duplicates a tag. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to duplicate.</param> /// <returns>The new tag.</returns> public TagInstance DuplicateTag(Stream stream, TagInstance tag) { if (tag == null) { throw new ArgumentNullException(nameof(tag)); } // Just extract the tag and add it back var result = AllocateTag(); SetTagDataRaw(stream, result, ExtractTagRaw(stream, tag)); return(result); }
/// <summary> /// Duplicates a tag. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to duplicate.</param> /// <returns>The new tag.</returns> public TagInstance DuplicateTag(Stream stream, TagInstance tag) { if (tag == null) { throw new ArgumentNullException("tag"); } // Just extract the tag and add it back var result = AllocateTag(); OverwriteFullTag(stream, result, ExtractFullTag(stream, tag)); return(result); }
/// <summary> /// Overwrites a tag's data. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to store.</param> public void SetTagData(Stream stream, TagInstance tag, TagData data) { if (tag == null) { throw new ArgumentNullException(nameof(tag)); } if (data == null) { throw new ArgumentNullException(nameof(data)); } if (data.Group == TagGroup.Null) { throw new ArgumentException("Cannot assign a tag to a null tag group"); } if (data.Data == null) { throw new ArgumentException("The tag data buffer is null"); } // Ensure the data fits var headerSize = TagInstance.CalculateHeaderSize(data); var alignedHeaderSize = (uint)((headerSize + 0xF) & ~0xF); if (tag.HeaderOffset < 0) { tag.HeaderOffset = GetNewTagOffset(tag.Index); } var alignedLength = (data.Data.Length + 0xF) & ~0xF; ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, alignedHeaderSize + alignedLength); tag.TotalSize = alignedHeaderSize + alignedLength; tag.Update(data, alignedHeaderSize); // Write in the new header and data stream.Position = tag.HeaderOffset; var writer = new BinaryWriter(stream); tag.WriteHeader(writer); StreamUtil.Fill(stream, 0, (int)(alignedHeaderSize - headerSize)); stream.Write(data.Data, 0, data.Data.Length); StreamUtil.Fill(stream, 0, alignedLength - data.Data.Length); // Correct pointers foreach (var fixup in data.PointerFixups) { writer.BaseStream.Position = tag.HeaderOffset + alignedHeaderSize + fixup.WriteOffset; writer.Write(tag.OffsetToPointer(alignedHeaderSize + fixup.TargetOffset)); } UpdateTagOffsets(writer); }
/// <summary> /// Saves any changes made to a tag's properties. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to update.</param> public void UpdateTag(Stream stream, TagInstance tag) { if (tag == null) { throw new ArgumentNullException("tag"); } if (tag.HeaderOffset < 0) { throw new ArgumentException("The tag is not in the cache file"); } var writer = new BinaryWriter(stream); UpdateTagHeader(writer, tag); UpdateTagFixups(writer, tag); UpdateTagOffsets(writer); }
/// <summary> /// Reads a tag's raw data from the file, including its header. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <returns>The data that was read.</returns> public byte[] ExtractTagRaw(Stream stream, TagInstance tag) { if (tag == null) { throw new ArgumentNullException(nameof(tag)); } if (tag.HeaderOffset < 0) { throw new ArgumentException("The tag is not in the cache file"); } stream.Position = tag.HeaderOffset; var result = new byte[tag.TotalSize]; stream.Read(result, 0, result.Length); return(result); }
/// <summary> /// Builds a description for a tag's data without extracting anything. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <param name="dataOffset">On return, this will contain the offset of the tag's data relative to its header.</param> /// <returns>The description that was built. </returns> private static TagData BuildTagDescription(Stream stream, TagInstance tag, out uint dataOffset) { var data = new TagData { Group = tag.Group, MainStructOffset = tag.MainStructOffset, }; foreach (var dependency in tag.Dependencies) { data.Dependencies.Add(dependency); } // Read pointer fixups var reader = new BinaryReader(stream); foreach (var pointerOffset in tag.PointerOffsets) { reader.BaseStream.Position = tag.HeaderOffset + pointerOffset; data.PointerFixups.Add(new TagPointerFixup { WriteOffset = pointerOffset, TargetOffset = tag.PointerToOffset(reader.ReadUInt32()), }); } // Find the start of the tag's data by finding the offset of the first block which is pointed to by something // We CAN'T just calculate a header size here because we don't know for sure if there's padding and how big it is var startOffset = tag.MainStructOffset; foreach (var fixup in data.PointerFixups) { startOffset = Math.Min(startOffset, Math.Min(fixup.WriteOffset, fixup.TargetOffset)); } // Now convert all offsets into relative ones foreach (var fixup in data.PointerFixups) { fixup.WriteOffset -= startOffset; fixup.TargetOffset -= startOffset; } data.ResourcePointerOffsets.AddRange(tag.ResourcePointerOffsets.Select(offset => offset - startOffset)); data.MainStructOffset -= startOffset; dataOffset = startOffset; return(data); }
/// <summary> /// Reads a tag's data from the file, not including its header. /// Any pointers in the tag will be adjusted to be relative to the start of the tag's data. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <returns>The data that was read.</returns> public byte[] ExtractTagData(Stream stream, TagInstance tag) { if (tag == null) { throw new ArgumentNullException("tag"); } if (tag.HeaderOffset < 0) { throw new ArgumentException("The tag is not in the cache file"); } stream.Position = tag.DataOffset; var result = new byte[tag.DataSize]; stream.Read(result, 0, result.Length); RebasePointers(tag, result, 0); return(result); }
/// <summary> /// Writes a tag's header back to the file, resizing it if necessary. /// </summary> /// <param name="writer">The stream to write to.</param> /// <param name="tag">The tag.</param> private void UpdateTagHeader(BinaryWriter writer, TagInstance tag) { // Resize the header if necessary var newHeaderSize = CalculateNewHeaderSize(tag); if (newHeaderSize != tag.HeaderSize) { ResizeBlock(writer.BaseStream, tag, tag.HeaderOffset, tag.HeaderSize, newHeaderSize); } tag.DataOffset = tag.HeaderOffset + newHeaderSize; // Write the tag header writer.BaseStream.Position = tag.HeaderOffset; writer.Write(tag.Checksum); writer.Write((uint)(tag.DataSize + newHeaderSize)); writer.Write((short)tag.Dependencies.Count); writer.Write((short)tag.DataFixups.Count); writer.Write((short)tag.ResourceFixups.Count); writer.Write((short)0); writer.Write(tag.MainStructOffset + newHeaderSize); writer.Write(tag.GroupTag.Value); writer.Write(tag.ParentGroupTag.Value); writer.Write(tag.GrandparentGroupTag.Value); writer.Write(tag.GroupName.Value); // Write dependencies foreach (var dependency in tag.Dependencies) { writer.Write(dependency); } // Write fixup pointers foreach (var fixup in tag.DataFixups.Concat(tag.ResourceFixups)) { writer.Write(fixup.WriteOffset + newHeaderSize + FixupPointerBase); } // Padding var paddingSize = (tag.HeaderSize - (writer.BaseStream.Position - tag.HeaderOffset)) & 0xF; for (var i = 0; i < paddingSize; i += 4) { writer.Write(0); } }
/// <summary> /// Overwrites a tag's data, including its header. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to overwrite the tag with.</param> /// <exception cref="System.ArgumentNullException">tag</exception> public void OverwriteFullTag(Stream stream, TagInstance tag, byte[] data) { if (tag == null) { throw new ArgumentNullException("tag"); } // Ensure the data fits if (tag.HeaderOffset < 0) { tag.HeaderOffset = GetNewTagOffset(tag.Index); } ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, data.Length); // Write the data stream.Position = tag.HeaderOffset; stream.Write(data, 0, data.Length); // Re-parse it and update tag offsets stream.Position = tag.HeaderOffset; ReadTagHeader(new BinaryReader(stream), tag); UpdateTagOffsets(new BinaryWriter(stream)); }
/// <summary> /// Reads the tags.dat file. /// </summary> /// <param name="reader">The stream to read from.</param> private void Load(BinaryReader reader) { // Read file header reader.BaseStream.Position = 0x4; var tagListOffset = reader.ReadInt32(); // 0x4 uint32 offset table offset var tagCount = reader.ReadInt32(); // 0x8 uint32 number of tags reader.BaseStream.Position = 0x10; Timestamp = reader.ReadInt64(); // 0x10 FILETIME timestamp // Read tag offset list var headerOffsets = new uint[tagCount]; reader.BaseStream.Position = tagListOffset; for (var i = 0; i < tagCount; i++) { headerOffsets[i] = reader.ReadUInt32(); } // Read each tag for (var i = 0; i < tagCount; i++) { if (headerOffsets[i] == 0) { // Offset of 0 = null tag _tags.Add(null); continue; } var tag = new TagInstance(i) { HeaderOffset = headerOffsets[i] }; _tags.Add(tag); reader.BaseStream.Position = tag.HeaderOffset; tag.ReadHeader(reader); } }
/// <summary> /// Overwrites a tag's raw data, including its header. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to overwrite the tag with.</param> /// <exception cref="System.ArgumentNullException">tag</exception> public void SetTagDataRaw(Stream stream, TagInstance tag, byte[] data) { if (tag == null) { throw new ArgumentNullException(nameof(tag)); } // Ensure the data fits if (tag.HeaderOffset < 0) { tag.HeaderOffset = GetNewTagOffset(tag.Index); } ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, data.Length); tag.TotalSize = data.Length; // Write the data stream.Position = tag.HeaderOffset; stream.Write(data, 0, data.Length); // Re-parse it stream.Position = tag.HeaderOffset; tag.ReadHeader(new BinaryReader(stream)); UpdateTagOffsets(new BinaryWriter(stream)); }
/// <summary> /// Saves any changes made to a tag's properties. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to update.</param> public void UpdateTag(Stream stream, TagInstance tag) { if (tag == null) throw new ArgumentNullException("tag"); if (tag.HeaderOffset < 0) throw new ArgumentException("The tag is not in the cache file"); var writer = new BinaryWriter(stream); UpdateTagHeader(writer, tag); UpdateTagFixups(writer, tag); UpdateTagOffsets(writer); }
/// <summary> /// Calculates the size of a tag's header after it will be updated in the file. /// </summary> /// <param name="tag">The tag.</param> /// <returns>The new size of the tag's header.</returns> private static uint CalculateNewHeaderSize(TagInstance tag) { return CalculateHeaderSize(tag.Dependencies.Count, tag.DataFixups.Count, tag.ResourceFixups.Count); }
/// <summary> /// Overwrites a tag's data, not including its header. /// Any pointers in the data to write will be adjusted to be relative to the start of the tag's header. /// Make sure that fixups are set correctly before calling this. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to write.</param> public void OverwriteTagData(Stream stream, TagInstance tag, byte[] data) { if (tag == null) throw new ArgumentNullException("tag"); // Ensure the data fits var newHeaderSize = CalculateNewHeaderSize(tag); if (tag.HeaderOffset < 0) tag.HeaderOffset = GetNewTagOffset(tag.Index); var alignedLength = (data.Length + 0xF) & ~0xF; ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, newHeaderSize + alignedLength); tag.DataOffset = tag.HeaderOffset + newHeaderSize; tag.DataSize = alignedLength; // Write it in and then update stream.Position = tag.DataOffset; stream.Write(data, 0, data.Length); StreamUtil.Fill(stream, 0, alignedLength - data.Length); UpdateTag(stream, tag); }
/// <summary> /// Resizes a block of tag data, updating relative pointers which do not point into the block. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag.</param> /// <param name="startOffset">The offset, from the start of the tag's data, where the block to resize begins at.</param> /// <param name="oldSize">The current size of the block to resize.</param> /// <param name="newSize">The new size of the block.</param> /// <param name="origin">The origin where data should be inserted or removed.</param> public void ResizeTagData(Stream stream, TagInstance tag, long startOffset, long oldSize, long newSize, ResizeOrigin origin) { if (tag == null) throw new ArgumentNullException("tag"); if (tag.HeaderOffset < 0) throw new ArgumentException("The tag is not in the cache file"); if (oldSize < 0) throw new ArgumentException("The old block size cannot be negative"); if (newSize < 0) throw new ArgumentException("Cannot resize a block to a negative size"); if (newSize == tag.DataSize) return; // Correct offsets pointing after the block var sizeDelta = newSize - oldSize; var blockEndOffset = startOffset + oldSize; tag.DataSize += sizeDelta; foreach (var fixup in tag.DataFixups.Concat(tag.ResourceFixups)) { if (fixup.WriteOffset >= blockEndOffset) fixup.WriteOffset = (uint)(fixup.WriteOffset + sizeDelta); if (fixup.TargetOffset >= blockEndOffset) fixup.TargetOffset = (uint)(fixup.TargetOffset + sizeDelta); } if (tag.MainStructOffset >= blockEndOffset) tag.MainStructOffset = (uint)(tag.MainStructOffset + sizeDelta); FixTagOffsets(tag.DataOffset + blockEndOffset, sizeDelta, tag); // Insert/remove the data long editOffset; if (origin == ResizeOrigin.Beginning) editOffset = startOffset; else if (sizeDelta > 0) editOffset = blockEndOffset; else editOffset = blockEndOffset + sizeDelta; stream.Position = tag.DataOffset + editOffset; if (sizeDelta > 0) StreamUtil.Insert(stream, (int)sizeDelta, 0); else StreamUtil.Remove(stream, (int)-sizeDelta); UpdateTag(stream, tag); }
/// <summary> /// Resizes a block of tag data, updating relative pointers which do not point into the block. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag.</param> /// <param name="startOffset">The offset, from the start of the tag's data, where the block to resize begins at.</param> /// <param name="oldSize">The current size of the block to resize.</param> /// <param name="newSize">The new size of the block.</param> /// <param name="origin">The origin where data should be inserted or removed.</param> public void ResizeTagData(Stream stream, TagInstance tag, long startOffset, long oldSize, long newSize, ResizeOrigin origin) { if (tag == null) { throw new ArgumentNullException("tag"); } if (tag.HeaderOffset < 0) { throw new ArgumentException("The tag is not in the cache file"); } if (oldSize < 0) { throw new ArgumentException("The old block size cannot be negative"); } if (newSize < 0) { throw new ArgumentException("Cannot resize a block to a negative size"); } if (newSize == tag.DataSize) { return; } // Correct offsets pointing after the block var sizeDelta = newSize - oldSize; var blockEndOffset = startOffset + oldSize; tag.DataSize += sizeDelta; foreach (var fixup in tag.DataFixups.Concat(tag.ResourceFixups)) { if (fixup.WriteOffset >= blockEndOffset) { fixup.WriteOffset = (uint)(fixup.WriteOffset + sizeDelta); } if (fixup.TargetOffset >= blockEndOffset) { fixup.TargetOffset = (uint)(fixup.TargetOffset + sizeDelta); } } if (tag.MainStructOffset >= blockEndOffset) { tag.MainStructOffset = (uint)(tag.MainStructOffset + sizeDelta); } FixTagOffsets(tag.DataOffset + blockEndOffset, sizeDelta, tag); // Insert/remove the data long editOffset; if (origin == ResizeOrigin.Beginning) { editOffset = startOffset; } else if (sizeDelta > 0) { editOffset = blockEndOffset; } else { editOffset = blockEndOffset + sizeDelta; } stream.Position = tag.DataOffset + editOffset; if (sizeDelta > 0) { StreamUtil.Insert(stream, (int)sizeDelta, 0); } else { StreamUtil.Remove(stream, (int)-sizeDelta); } UpdateTag(stream, tag); }
/// <summary> /// Overwrites a tag's data, including its header. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to overwrite the tag with.</param> /// <exception cref="System.ArgumentNullException">tag</exception> public void OverwriteFullTag(Stream stream, TagInstance tag, byte[] data) { if (tag == null) throw new ArgumentNullException("tag"); // Ensure the data fits if (tag.HeaderOffset < 0) tag.HeaderOffset = GetNewTagOffset(tag.Index); ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, data.Length); // Write the data stream.Position = tag.HeaderOffset; stream.Write(data, 0, data.Length); // Re-parse it and update tag offsets stream.Position = tag.HeaderOffset; ReadTagHeader(new BinaryReader(stream), tag); UpdateTagOffsets(new BinaryWriter(stream)); }
/// <summary> /// Writes a tag's header back to the file, resizing it if necessary. /// </summary> /// <param name="writer">The stream to write to.</param> /// <param name="tag">The tag.</param> private void UpdateTagHeader(BinaryWriter writer, TagInstance tag) { // Resize the header if necessary var newHeaderSize = CalculateNewHeaderSize(tag); if (newHeaderSize != tag.HeaderSize) ResizeBlock(writer.BaseStream, tag, tag.HeaderOffset, tag.HeaderSize, newHeaderSize); tag.DataOffset = tag.HeaderOffset + newHeaderSize; // Write the tag header writer.BaseStream.Position = tag.HeaderOffset; writer.Write(tag.Checksum); writer.Write((uint)(tag.DataSize + newHeaderSize)); writer.Write((short)tag.Dependencies.Count); writer.Write((short)tag.DataFixups.Count); writer.Write((short)tag.ResourceFixups.Count); writer.Write((short)0); writer.Write(tag.MainStructOffset + newHeaderSize); writer.Write(tag.GroupTag.Value); writer.Write(tag.ParentGroupTag.Value); writer.Write(tag.GrandparentGroupTag.Value); writer.Write(tag.GroupName.Value); // Write dependencies foreach (var dependency in tag.Dependencies) writer.Write(dependency); // Write fixup pointers foreach (var fixup in tag.DataFixups.Concat(tag.ResourceFixups)) writer.Write(fixup.WriteOffset + newHeaderSize + FixupPointerBase); // Padding var paddingSize = (tag.HeaderSize - (writer.BaseStream.Position - tag.HeaderOffset)) & 0xF; for (var i = 0; i < paddingSize; i += 4) writer.Write(0); }
/// <summary> /// Builds a description for a tag's data without extracting anything. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <param name="dataOffset">On return, this will contain the offset of the tag's data relative to its header.</param> /// <returns>The description that was built. </returns> private static TagData BuildTagDescription(Stream stream, TagInstance tag, out uint dataOffset) { var data = new TagData { Group = tag.Group, MainStructOffset = tag.MainStructOffset, }; foreach (var dependency in tag.Dependencies) data.Dependencies.Add(dependency); // Read pointer fixups var reader = new BinaryReader(stream); foreach (var pointerOffset in tag.PointerOffsets) { reader.BaseStream.Position = tag.HeaderOffset + pointerOffset; data.PointerFixups.Add(new TagPointerFixup { WriteOffset = pointerOffset, TargetOffset = tag.PointerToOffset(reader.ReadUInt32()), }); } // Find the start of the tag's data by finding the offset of the first block which is pointed to by something // We CAN'T just calculate a header size here because we don't know for sure if there's padding and how big it is var startOffset = tag.MainStructOffset; foreach (var fixup in data.PointerFixups) startOffset = Math.Min(startOffset, Math.Min(fixup.WriteOffset, fixup.TargetOffset)); // Now convert all offsets into relative ones foreach (var fixup in data.PointerFixups) { fixup.WriteOffset -= startOffset; fixup.TargetOffset -= startOffset; } data.ResourcePointerOffsets.AddRange(tag.ResourcePointerOffsets.Select(offset => offset - startOffset)); data.MainStructOffset -= startOffset; dataOffset = startOffset; return data; }
/// <summary> /// Reads the tags.dat file. /// </summary> /// <param name="reader">The stream to read from.</param> private void Load(BinaryReader reader) { // Read file header reader.BaseStream.Position = 0x4; var tagListOffset = reader.ReadInt32(); // 0x4 uint32 offset table offset var tagCount = reader.ReadInt32(); // 0x8 uint32 number of tags reader.BaseStream.Position = 0x10; Timestamp = reader.ReadInt64(); // 0x10 FILETIME timestamp // Read tag offset list var headerOffsets = new uint[tagCount]; reader.BaseStream.Position = tagListOffset; for (var i = 0; i < tagCount; i++) headerOffsets[i] = reader.ReadUInt32(); // Read each tag for (var i = 0; i < tagCount; i++) { if (headerOffsets[i] == 0) { // Offset of 0 = null tag _tags.Add(null); continue; } var tag = new TagInstance { Index = i, HeaderOffset = headerOffsets[i] }; _tags.Add(tag); reader.BaseStream.Position = tag.HeaderOffset; ReadTagHeader(reader, tag); } }
/// <summary> /// Overwrites a tag's raw data, including its header. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to overwrite the tag with.</param> /// <exception cref="System.ArgumentNullException">tag</exception> public void SetTagDataRaw(Stream stream, TagInstance tag, byte[] data) { if (tag == null) throw new ArgumentNullException(nameof(tag)); // Ensure the data fits if (tag.HeaderOffset < 0) tag.HeaderOffset = GetNewTagOffset(tag.Index); ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, data.Length); tag.TotalSize = data.Length; // Write the data stream.Position = tag.HeaderOffset; stream.Write(data, 0, data.Length); // Re-parse it stream.Position = tag.HeaderOffset; tag.ReadHeader(new BinaryReader(stream)); UpdateTagOffsets(new BinaryWriter(stream)); }
/// <summary> /// Overwrites a tag's data. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to overwrite.</param> /// <param name="data">The data to store.</param> public void SetTagData(Stream stream, TagInstance tag, TagData data) { if (tag == null) throw new ArgumentNullException(nameof(tag)); if (data == null) throw new ArgumentNullException(nameof(data)); if (data.Group == TagGroup.Null) throw new ArgumentException("Cannot assign a tag to a null tag group"); if (data.Data == null) throw new ArgumentException("The tag data buffer is null"); // Ensure the data fits var headerSize = TagInstance.CalculateHeaderSize(data); var alignedHeaderSize = (uint)((headerSize + 0xF) & ~0xF); if (tag.HeaderOffset < 0) tag.HeaderOffset = GetNewTagOffset(tag.Index); var alignedLength = (data.Data.Length + 0xF) & ~0xF; ResizeBlock(stream, tag, tag.HeaderOffset, tag.TotalSize, alignedHeaderSize + alignedLength); tag.TotalSize = alignedHeaderSize + alignedLength; tag.Update(data, alignedHeaderSize); // Write in the new header and data stream.Position = tag.HeaderOffset; var writer = new BinaryWriter(stream); tag.WriteHeader(writer); StreamUtil.Fill(stream, 0, (int)(alignedHeaderSize - headerSize)); stream.Write(data.Data, 0, data.Data.Length); StreamUtil.Fill(stream, 0, alignedLength - data.Data.Length); // Correct pointers foreach (var fixup in data.PointerFixups) { writer.BaseStream.Position = tag.HeaderOffset + alignedHeaderSize + fixup.WriteOffset; writer.Write(tag.OffsetToPointer(alignedHeaderSize + fixup.TargetOffset)); } UpdateTagOffsets(writer); }
/// <summary> /// Reads a tag's data from the file. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <returns>The data that was read.</returns> public TagData ExtractTag(Stream stream, TagInstance tag) { if (tag == null) throw new ArgumentNullException(nameof(tag)); if (tag.HeaderOffset < 0) throw new ArgumentException("The tag is not in the cache file"); // Build the description info and get the data offset uint dataOffset; var data = BuildTagDescription(stream, tag, out dataOffset); // Read the tag data stream.Position = tag.HeaderOffset + dataOffset; data.Data = new byte[tag.TotalSize - dataOffset]; stream.Read(data.Data, 0, data.Data.Length); // Correct pointers using (var dataWriter = new BinaryWriter(new MemoryStream(data.Data))) { foreach (var fixup in data.PointerFixups) { dataWriter.BaseStream.Position = fixup.WriteOffset; dataWriter.Write(tag.OffsetToPointer(fixup.TargetOffset)); } } return data; }
/// <summary> /// Reads a tag's header. /// </summary> /// <param name="reader">The stream to read from.</param> /// <param name="resultTag">The tag to update.</param> private static void ReadTagHeader(BinaryReader reader, TagInstance resultTag) { resultTag.Checksum = reader.ReadUInt32(); // 0x00 uint32 checksum var totalSize = reader.ReadUInt32(); // 0x04 uint32 total size var numDependencies = reader.ReadInt16(); // 0x08 int16 dependencies count var numDataFixups = reader.ReadInt16(); // 0x0A int16 data fixup count var numResourceFixups = reader.ReadInt16(); // 0x0C int16 resource fixup count reader.BaseStream.Position += 2; // 0x0E int16 (padding) var mainStructOffset = reader.ReadUInt32(); // 0x10 uint32 main struct offset resultTag.GroupTag = new Tag(reader.ReadInt32()); // 0x14 int32 group tag resultTag.ParentGroupTag = new Tag(reader.ReadInt32()); // 0x18 int32 parent group tag resultTag.GrandparentGroupTag = new Tag(reader.ReadInt32()); // 0x1C int32 grandparent group tag resultTag.GroupName = new StringId(reader.ReadUInt32()); // 0x20 uint32 group name stringid // Read dependencies resultTag.Dependencies.Clear(); for (var j = 0; j < numDependencies; j++) resultTag.Dependencies.Add(reader.ReadInt32()); // Read fixup pointers resultTag.DataFixups.Clear(); for (var j = 0; j < numDataFixups; j++) resultTag.DataFixups.Add(new TagFixup { WriteOffset = reader.ReadUInt32() - FixupPointerBase }); resultTag.ResourceFixups.Clear(); for (var j = 0; j < numResourceFixups; j++) resultTag.ResourceFixups.Add(new TagFixup { WriteOffset = reader.ReadUInt32() - FixupPointerBase }); // Read fixup destinations foreach (var fixup in resultTag.DataFixups) fixup.TargetOffset = ReadFixupTarget(reader, fixup.WriteOffset, resultTag.HeaderOffset); foreach (var fixup in resultTag.ResourceFixups) fixup.TargetOffset = ReadFixupTarget(reader, fixup.WriteOffset, resultTag.HeaderOffset); // Compute the data offset based on the smallest offset into the tag var smallestOffset = mainStructOffset; foreach (var fixup in resultTag.DataFixups.Concat(resultTag.ResourceFixups)) { if (fixup.WriteOffset < smallestOffset) smallestOffset = fixup.WriteOffset; if (fixup.TargetOffset < smallestOffset) smallestOffset = fixup.TargetOffset; } resultTag.MainStructOffset = mainStructOffset - smallestOffset; resultTag.DataOffset = resultTag.HeaderOffset + smallestOffset; resultTag.DataSize = totalSize - smallestOffset; // Make fixups relative to the data offset foreach (var fixup in resultTag.DataFixups.Concat(resultTag.ResourceFixups)) { fixup.TargetOffset -= smallestOffset; fixup.WriteOffset -= smallestOffset; } }
/// <summary> /// Calculates the size of a tag's header after it will be updated in the file. /// </summary> /// <param name="tag">The tag.</param> /// <returns>The new size of the tag's header.</returns> private static uint CalculateNewHeaderSize(TagInstance tag) { return(CalculateHeaderSize(tag.Dependencies.Count, tag.DataFixups.Count, tag.ResourceFixups.Count)); }
/// <summary> /// Reads a tag's data from the file, not including its header. /// Any pointers in the tag will be adjusted to be relative to the start of the tag's data. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="tag">The tag to read.</param> /// <returns>The data that was read.</returns> public byte[] ExtractTagData(Stream stream, TagInstance tag) { if (tag == null) throw new ArgumentNullException("tag"); if (tag.HeaderOffset < 0) throw new ArgumentException("The tag is not in the cache file"); stream.Position = tag.DataOffset; var result = new byte[tag.DataSize]; stream.Read(result, 0, result.Length); RebasePointers(tag, result, 0); return result; }
/// <summary> /// Fixes tag offsets after a resize operation. /// </summary> /// <param name="startOffset">The offset where the resize operation took place.</param> /// <param name="sizeDelta">The amount to add to each tag offset after the start offset.</param> /// <param name="ignore">A tag to ignore.</param> private void FixTagOffsets(long startOffset, long sizeDelta, TagInstance ignore) { foreach (var adjustTag in _tags.Where(t => t != null && t != ignore && t.HeaderOffset >= startOffset)) { adjustTag.HeaderOffset += sizeDelta; adjustTag.DataOffset += sizeDelta; } }
/// <summary> /// Reads a tag's header. /// </summary> /// <param name="reader">The stream to read from.</param> /// <param name="resultTag">The tag to update.</param> private static void ReadTagHeader(BinaryReader reader, TagInstance resultTag) { resultTag.Checksum = reader.ReadUInt32(); // 0x00 uint32 checksum var totalSize = reader.ReadUInt32(); // 0x04 uint32 total size var numDependencies = reader.ReadInt16(); // 0x08 int16 dependencies count var numDataFixups = reader.ReadInt16(); // 0x0A int16 data fixup count var numResourceFixups = reader.ReadInt16(); // 0x0C int16 resource fixup count reader.BaseStream.Position += 2; // 0x0E int16 (padding) var mainStructOffset = reader.ReadUInt32(); // 0x10 uint32 main struct offset resultTag.GroupTag = new Tag(reader.ReadInt32()); // 0x14 int32 group tag resultTag.ParentGroupTag = new Tag(reader.ReadInt32()); // 0x18 int32 parent group tag resultTag.GrandparentGroupTag = new Tag(reader.ReadInt32()); // 0x1C int32 grandparent group tag resultTag.GroupName = new StringId(reader.ReadUInt32()); // 0x20 uint32 group name stringid // Read dependencies resultTag.Dependencies.Clear(); for (var j = 0; j < numDependencies; j++) { resultTag.Dependencies.Add(reader.ReadInt32()); } // Read fixup pointers resultTag.DataFixups.Clear(); for (var j = 0; j < numDataFixups; j++) { resultTag.DataFixups.Add(new TagFixup { WriteOffset = reader.ReadUInt32() - FixupPointerBase }); } resultTag.ResourceFixups.Clear(); for (var j = 0; j < numResourceFixups; j++) { resultTag.ResourceFixups.Add(new TagFixup { WriteOffset = reader.ReadUInt32() - FixupPointerBase }); } // Read fixup destinations foreach (var fixup in resultTag.DataFixups) { fixup.TargetOffset = ReadFixupTarget(reader, fixup.WriteOffset, resultTag.HeaderOffset); } foreach (var fixup in resultTag.ResourceFixups) { fixup.TargetOffset = ReadFixupTarget(reader, fixup.WriteOffset, resultTag.HeaderOffset); } // Compute the data offset based on the smallest offset into the tag var smallestOffset = mainStructOffset; foreach (var fixup in resultTag.DataFixups.Concat(resultTag.ResourceFixups)) { if (fixup.WriteOffset < smallestOffset) { smallestOffset = fixup.WriteOffset; } if (fixup.TargetOffset < smallestOffset) { smallestOffset = fixup.TargetOffset; } } resultTag.MainStructOffset = mainStructOffset - smallestOffset; resultTag.DataOffset = resultTag.HeaderOffset + smallestOffset; resultTag.DataSize = totalSize - smallestOffset; // Make fixups relative to the data offset foreach (var fixup in resultTag.DataFixups.Concat(resultTag.ResourceFixups)) { fixup.TargetOffset -= smallestOffset; fixup.WriteOffset -= smallestOffset; } }
/// <summary> /// Resizes a block of data in the file and updates tag offsets. /// </summary> /// <param name="stream">The stream.</param> /// <param name="tag">The tag that the block belongs to, if any.</param> /// <param name="startOffset">The offset where the block to resize begins at.</param> /// <param name="oldSize">The current size of the block to resize.</param> /// <param name="newSize">The new size of the block.</param> /// <exception cref="System.ArgumentException">Cannot resize a block to a negative size</exception> private void ResizeBlock(Stream stream, TagInstance tag, long startOffset, long oldSize, long newSize) { if (newSize < 0) throw new ArgumentException("Cannot resize a block to a negative size"); var oldEndOffset = startOffset + oldSize; var sizeDelta = newSize - oldSize; StreamUtil.Copy(stream, oldEndOffset, oldEndOffset + sizeDelta, stream.Length - oldEndOffset); FixTagOffsets(oldEndOffset, sizeDelta, tag); }
/// <summary> /// Duplicates a tag. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to duplicate.</param> /// <returns>The new tag.</returns> public TagInstance DuplicateTag(Stream stream, TagInstance tag) { if (tag == null) throw new ArgumentNullException(nameof(tag)); // Just extract the tag and add it back var result = AllocateTag(); SetTagDataRaw(stream, result, ExtractTagRaw(stream, tag)); return result; }
/// <summary> /// Allocates a new tag at the end of the tag list without updating the file. /// You can give the tag data by using one of the overwrite functions. /// </summary> /// <returns>The allocated tag.</returns> public TagInstance AllocateTag() { var tagIndex = _tags.Count; var tag = new TagInstance { Index = tagIndex }; _tags.Add(tag); return tag; }
/// <summary> /// Duplicates a tag. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag to duplicate.</param> /// <returns>The new tag.</returns> public TagInstance DuplicateTag(Stream stream, TagInstance tag) { if (tag == null) throw new ArgumentNullException("tag"); // Just extract the tag and add it back var result = AllocateTag(); OverwriteFullTag(stream, result, ExtractFullTag(stream, tag)); return result; }