/// <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, HaloTag tag) { // Resize the header if necessary var newHeaderSize = CalculateHeaderSize(tag.Dependencies.Count, tag.DataFixups.Count, tag.ResourceFixups.Count); var oldHeaderSize = GetHeaderSize(tag); if (newHeaderSize > oldHeaderSize) ResizeTag(writer.BaseStream, tag, 0, (int)newHeaderSize - (int)oldHeaderSize, InsertOrigin.After, ResizeMode.Insert); // Write the tag header // See TagCacheReader for more info on this layout var newHeaderOffset = tag.Offset - newHeaderSize; _headerOffsets[tag.Index] = newHeaderOffset; writer.BaseStream.Position = newHeaderOffset; writer.Write(tag.Checksum); writer.Write(tag.Size + 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.Class.Value); writer.Write(tag.ParentClass.Value); writer.Write(tag.GrandparentClass.Value); writer.Write(tag.ClassId); // 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); }
private void ReadTagHeader(BinaryReader reader, HaloTag resultTag) { var headerOffset = (uint)reader.BaseStream.Position; var 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 var tagClass = new MagicNumber(reader.ReadInt32()); // 0x14 int32 class var parentClass = new MagicNumber(reader.ReadInt32()); // 0x18 int32 parent class var grandparentClass = new MagicNumber(reader.ReadInt32()); // 0x1C int32 grandparent class var classId = reader.ReadInt32(); // 0x20 uint32 class stringid var totalHeaderSize = CalculateHeaderSize(numDependencies, numDataFixups, numResourceFixups); // Update the tag object _tagClasses.Add(tagClass); resultTag.Class = tagClass; resultTag.ParentClass = parentClass; resultTag.GrandparentClass = grandparentClass; resultTag.MainStructOffset = mainStructOffset - totalHeaderSize; resultTag.Offset = headerOffset + totalHeaderSize; resultTag.Size = totalSize - totalHeaderSize; resultTag.Checksum = checksum; resultTag.ClassId = classId; // Read dependencies resultTag.Dependencies.Clear(); for (var j = 0; j < numDependencies; j++) resultTag.Dependencies.Add(reader.ReadInt32()); // Read fixup pointers var dataFixupPointers = new uint[numDataFixups]; for (var j = 0; j < numDataFixups; j++) dataFixupPointers[j] = reader.ReadUInt32(); var resourceFixupPointers = new uint[numResourceFixups]; for (var j = 0; j < numResourceFixups; j++) resourceFixupPointers[j] = reader.ReadUInt32(); // Process fixups resultTag.DataFixups.Clear(); resultTag.ResourceFixups.Clear(); foreach (var fixup in dataFixupPointers) resultTag.DataFixups.Add(ReadFixup(reader, fixup, headerOffset, totalHeaderSize)); foreach (var fixup in resourceFixupPointers) resultTag.ResourceFixups.Add(ReadFixup(reader, fixup, headerOffset, totalHeaderSize)); }
/// <summary> /// Inserts or removes data in a tag. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag.</param> /// <param name="insertOffset">The offset, from the start of the tag's header, to insert data at.</param> /// <param name="sizeDelta">The size of the data to insert or remove. If positive, data will be inserted. If negative, data will be removed.</param> /// <param name="origin">The type of resize to perform. See <see cref="InsertOrigin"/>.</param> /// <param name="mode">The resize mode. See <see cref="ResizeMode"/>.</param> private void ResizeTag(Stream stream, HaloTag tag, uint insertOffset, int sizeDelta, InsertOrigin origin, ResizeMode mode) { if (sizeDelta == 0) return; var headerSize = GetHeaderSize(tag); if (sizeDelta < 0 && ((origin == InsertOrigin.Before && -sizeDelta > insertOffset) || (origin == InsertOrigin.After && insertOffset + -sizeDelta > headerSize + tag.Size))) throw new ArgumentException("Cannot remove more bytes than there are available in the tag"); // In insertion mode, correct relative offsets to account for inserted data var relativeCompareOffset = (origin == InsertOrigin.Before) ? insertOffset : insertOffset + 1; // hack if (headerSize < relativeCompareOffset) { tag.Size = (uint)(tag.Size + sizeDelta); if (mode == ResizeMode.Insert) { foreach (var fixup in tag.DataFixups.Concat(tag.ResourceFixups)) { if (fixup.WriteOffset + headerSize >= relativeCompareOffset) fixup.WriteOffset = (uint)(fixup.WriteOffset + sizeDelta); if (fixup.TargetOffset + headerSize >= relativeCompareOffset) fixup.TargetOffset = (uint)(fixup.TargetOffset + sizeDelta); } if (tag.MainStructOffset + headerSize >= relativeCompareOffset) tag.MainStructOffset = (uint)(tag.MainStructOffset + sizeDelta); } } // Correct tag offsets var absoluteOffset = _headerOffsets[tag.Index] + insertOffset; var absoluteCompareOffset = (origin == InsertOrigin.Before) ? absoluteOffset : absoluteOffset + 1; // hack for (var i = 0; i < _tags.Count; i++) { if (_tags[i] == null) continue; if (_headerOffsets[i] >= absoluteCompareOffset) // Header offset (absolute) _headerOffsets[i] = (uint)(_headerOffsets[i] + sizeDelta); if (_tags[i].Offset >= absoluteCompareOffset) // Data offset (absolute) _tags[i].Offset = (uint)(_tags[i].Offset + sizeDelta); } // Insert/remove the data if (sizeDelta < 0 && origin == InsertOrigin.Before) absoluteOffset = (uint)(absoluteOffset + sizeDelta); stream.Position = absoluteOffset; if (sizeDelta > 0) StreamUtil.Insert(stream, sizeDelta, 0); else StreamUtil.Remove(stream, -sizeDelta); }
/// <summary> /// Gets the current size of a tag's header. /// </summary> /// <param name="tag">The tag.</param> /// <returns>The current size of the tag's header in bytes.</returns> private uint GetHeaderSize(HaloTag tag) { return tag.Offset - _headerOffsets[tag.Index]; }
/// <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 // Read tag offset list reader.BaseStream.Position = tagListOffset; for (var i = 0; i < tagCount; i++) _headerOffsets.Add(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; } reader.BaseStream.Position = _headerOffsets[i]; var tag = new HaloTag { Index = i }; _tags.Add(tag); ReadTagHeader(reader, 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(HaloTag 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> /// 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, HaloTag tag) { var totalHeaderSize = CalculateHeaderSize(tag.Dependencies.Count, tag.DataFixups.Count, tag.ResourceFixups.Count); foreach (var fixup in tag.DataFixups) { writer.BaseStream.Position = tag.Offset + fixup.WriteOffset; writer.Write(fixup.TargetOffset + totalHeaderSize + FixupPointerBase); } }
/// <summary> /// Resizes a tag's data. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag.</param> /// <param name="newSize">The new size of the tag's data.</param> public void ResizeTagData(Stream stream, HaloTag tag, uint newSize) { if (tag == null) throw new ArgumentNullException("tag"); if (newSize == tag.Size) return; // Resize at the end of the tag var headerSize = GetHeaderSize(tag); ResizeTag(stream, tag, headerSize + tag.Size, (int)(newSize - tag.Size), InsertOrigin.Before, ResizeMode.Resize); // Update only the header and tag offset table - a full update isn't needed var writer = new BinaryWriter(stream); UpdateTagHeader(writer, tag); 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, HaloTag tag) { if (tag == null) throw new ArgumentNullException("tag"); var writer = new BinaryWriter(stream); UpdateTagHeader(writer, tag); UpdateTagFixups(writer, tag); UpdateTagOffsets(writer); }
/// <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. /// </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 OverwriteTag(Stream stream, HaloTag tag, byte[] data) { if (tag == null) throw new ArgumentNullException("tag"); // Adjust fixups before injecting the data using (var reader = new BinaryReader(new MemoryStream(data))) { for (var i = 0; i < tag.DataFixups.Count; i++) { var fixup = tag.DataFixups[i]; reader.BaseStream.Position = fixup.WriteOffset; var newPointer = reader.ReadUInt32(); if (newPointer == 0) { // Pointer was nulled - removed it tag.DataFixups.RemoveAt(i); i--; continue; } fixup.TargetOffset = newPointer - FixupPointerBase; } } RebasePointers(tag, data, GetHeaderSize(tag)); // Resize the tag and write the data ResizeTagData(stream, tag, (uint)data.Length); stream.Position = tag.Offset; stream.Write(data, 0, data.Length); }
/// <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 OverwriteTagWithHeader(Stream stream, HaloTag tag, byte[] data) { if (tag == null) throw new ArgumentNullException("tag"); // Ensure the data fits var oldHeaderSize = GetHeaderSize(tag); var oldTagSize = oldHeaderSize + tag.Size; ResizeTag(stream, tag, oldTagSize, (int)(data.Length - oldTagSize), InsertOrigin.Before, ResizeMode.Resize); // Write the data stream.Position = _headerOffsets[tag.Index]; stream.Write(data, 0, data.Length); // Re-parse it and update tag offsets stream.Position = _headerOffsets[tag.Index]; ReadTagHeader(new BinaryReader(stream), tag); UpdateTagOffsets(new BinaryWriter(stream)); }
/// <summary> /// Inserts or removes data in a tag and then updates it if necessary. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="tag">The tag.</param> /// <param name="insertOffset">The offset, from the start of the tag's data, to insert data at.</param> /// <param name="sizeDelta">The size of the data to insert or remove. If positive, data will be inserted. If negative, data will be removed.</param> /// <param name="origin">The type of resize to perform. See <see cref="InsertOrigin"/>.</param> public void InsertTagData(Stream stream, HaloTag tag, uint insertOffset, int sizeDelta, InsertOrigin origin) { if (tag == null) throw new ArgumentNullException("tag"); if (sizeDelta == 0) return; ResizeTag(stream, tag, insertOffset + GetHeaderSize(tag), sizeDelta, origin, ResizeMode.Insert); UpdateTag(stream, tag); }
/// <summary> /// Reads a tag's 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[] ExtractTagWithHeader(Stream stream, HaloTag tag) { if (tag == null) throw new ArgumentNullException("tag"); var headerSize = GetHeaderSize(tag); stream.Position = tag.Offset - headerSize; var result = new byte[tag.Size + headerSize]; stream.Read(result, 0, result.Length); return result; }
/// <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[] ExtractTag(Stream stream, HaloTag tag) { if (tag == null) throw new ArgumentNullException("tag"); stream.Position = tag.Offset; var result = new byte[tag.Size]; stream.Read(result, 0, result.Length); RebasePointers(tag, result, 0); 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 HaloTag DuplicateTag(Stream stream, HaloTag tag) { if (tag == null) throw new ArgumentNullException("tag"); // Just extract the tag and add it back return AddTag(stream, ExtractTagWithHeader(stream, tag)); }
/// <summary> /// Adds a tag to the file. /// </summary> /// <param name="stream">The stream to write to.</param> /// <param name="data">The tag data. Must include the header.</param> /// <returns>The new tag.</returns> public HaloTag AddTag(Stream stream, byte[] data) { // Push back the data at the end of the file and write the tag in after the last tag var newTagOffset = GetTagDataEndOffset(); StreamUtil.Copy(stream, newTagOffset, newTagOffset + data.Length, stream.Length - newTagOffset); stream.Position = newTagOffset; stream.Write(data, 0, data.Length); // Create an object for the tag and add it to the tag list var tagIndex = _tags.Count; var newTag = new HaloTag { Index = tagIndex }; _headerOffsets.Add(newTagOffset); _tags.Add(newTag); // Read the tag's header from the stream stream.Position = newTagOffset; ReadTagHeader(new BinaryReader(stream), newTag); // Update the tag offset table UpdateTagOffsets(new BinaryWriter(stream)); return newTag; }