diff --git a/Mutagen.Bethesda.Core.UnitTests/Plugins/Binary/ListGroupOverlayTests.cs b/Mutagen.Bethesda.Core.UnitTests/Plugins/Binary/ListGroupOverlayTests.cs new file mode 100644 index 0000000000..c1bb70a167 --- /dev/null +++ b/Mutagen.Bethesda.Core.UnitTests/Plugins/Binary/ListGroupOverlayTests.cs @@ -0,0 +1,142 @@ +using Mutagen.Bethesda.Plugins; +using Mutagen.Bethesda.Plugins.Binary.Overlay; +using Mutagen.Bethesda.Plugins.Binary.Streams; +using Mutagen.Bethesda.Plugins.Binary.Translations; +using Mutagen.Bethesda.Plugins.Exceptions; +using Mutagen.Bethesda.Plugins.Masters; +using Mutagen.Bethesda.Plugins.Meta; +using Mutagen.Bethesda.Plugins.Records; +using Mutagen.Bethesda.Plugins.Records.Internals; +using Mutagen.Bethesda.Testing.AutoData; +using Shouldly; +using Xunit; + +namespace Mutagen.Bethesda.UnitTests.Plugins.Binary; + +public abstract class AListGroupOverlayTests +{ + protected abstract GameRelease Release { get; } + + private GameConstants Constants => GameConstants.Get(Release); + + private byte[] Written(Action write) + { + var stream = new MemoryStream(); + using (var writer = new MutagenWriter(stream, Constants)) + { + write(writer); + } + return stream.ToArray(); + } + + private byte[] ListGroupHolding(Action writeEntry) => Written(writer => + { + using (HeaderExport.Group(writer, RecordTypes.GRUP)) + { + writer.WriteZeros(Constants.GroupConstants.LengthAfterLength); + writeEntry(writer); + } + }); + + private void WriteGroupShorterThanItsHeader(MutagenWriter writer) + { + writer.Write(RecordTypes.GRUP.TypeInt); + writer.WriteZeros(Constants.GroupConstants.LengthAfterType); + } + + private ParsingMeta Meta(ModKey modKey) => new( + Constants, + modKey, + SeparatedMasterPackage.NotSeparate(new MasterReferenceCollection(modKey))); + + private IReadOnlyList ParseLocations(byte[] listGroup, ModKey modKey) + { + var meta = Meta(modKey); + var stream = new OverlayStream(listGroup, meta); + stream.Position = Constants.GroupConstants.HeaderLength; + return GroupListOverlay.Factory( + stream, + listGroup, + new BinaryOverlayFactoryPackage(meta), + offset: 0); + } + + private IReadOnlyList ParseSubBlockLocations(byte[] subBlocks, ModKey modKey) + { + return PluginBinaryOverlay.ParseRecordLocations( + new OverlayStream(subBlocks, Meta(modKey)), + trigger: RecordTypes.GRUP, + constants: Constants.GroupConstants, + skipHeader: false); + } + + [Theory, MutagenAutoData] + public void RecordWhereAGroupBelongs_ThrowsNamingTheRecordType(ModKey modKey) + { + var listGroup = ListGroupHolding(writer => + { + using (HeaderExport.Record(writer, RecordTypes.MISC)) + { + writer.WriteZeros(Constants.MajorConstants.LengthAfterLength); + } + }); + + Should.Throw(() => ParseLocations(listGroup, modKey)) + .Message.ShouldContain(RecordTypes.MISC.Type); + } + + [Theory, MutagenAutoData] + public void GroupShorterThanItsHeader_ThrowsAsMalformed(ModKey modKey) + { + var listGroup = ListGroupHolding(WriteGroupShorterThanItsHeader); + + Should.Throw(() => ParseLocations(listGroup, modKey)); + } + + [Theory, MutagenAutoData] + public void SubBlockShorterThanItsHeader_ThrowsAsMalformed(ModKey modKey) + { + var subBlocks = Written(WriteGroupShorterThanItsHeader); + + Should.Throw(() => ParseSubBlockLocations(subBlocks, modKey)); + } + + [Theory, MutagenAutoData] + public void EmptyGroup_ReadsAsOneEntry(ModKey modKey) + { + var listGroup = ListGroupHolding(writer => + { + using (HeaderExport.Group(writer, RecordTypes.GRUP)) + { + writer.WriteZeros(Constants.GroupConstants.LengthAfterLength); + } + }); + + ParseLocations(listGroup, modKey).Count.ShouldBe(1); + } +} + +public class OblivionListGroupOverlayTests : AListGroupOverlayTests +{ + protected override GameRelease Release => GameRelease.Oblivion; +} + +public class Fallout3ListGroupOverlayTests : AListGroupOverlayTests +{ + protected override GameRelease Release => GameRelease.Fallout3; +} + +public class SkyrimListGroupOverlayTests : AListGroupOverlayTests +{ + protected override GameRelease Release => GameRelease.SkyrimSE; +} + +public class Fallout4ListGroupOverlayTests : AListGroupOverlayTests +{ + protected override GameRelease Release => GameRelease.Fallout4; +} + +public class StarfieldListGroupOverlayTests : AListGroupOverlayTests +{ + protected override GameRelease Release => GameRelease.Starfield; +} diff --git a/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/AListGroupOverlay.cs b/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/AListGroupOverlay.cs index 5b72f38494..7f0fe58633 100644 --- a/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/AListGroupOverlay.cs +++ b/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/AListGroupOverlay.cs @@ -1,12 +1,12 @@ using Mutagen.Bethesda.Plugins.Binary.Headers; using Mutagen.Bethesda.Plugins.Binary.Translations; +using Mutagen.Bethesda.Plugins.Exceptions; using Mutagen.Bethesda.Plugins.Internals; using Noggog; using System.Buffers.Binary; using System.Collections; using System.IO.Compression; using Loqui; -using Mutagen.Bethesda.Plugins.Meta; using Mutagen.Bethesda.Plugins.Records; using Mutagen.Bethesda.Plugins.Assets; @@ -61,7 +61,6 @@ public static GroupListOverlay Factory( IBinaryReadStream stream, ReadOnlyMemorySlice data, BinaryOverlayFactoryPackage package, - ObjectType objectType, int offset) { List locations = new List(); @@ -73,9 +72,19 @@ public static GroupListOverlay Factory( // Parse locations while (stream.Position < finalPos) { - VariableHeader meta = package.MetaData.Constants.VariableHeader(stream.RemainingMemory, objectType); + var groupHeader = package.MetaData.Constants.GroupHeader(stream.RemainingMemory); + if (!groupHeader.IsGroup) + { + var majorMeta = package.MetaData.Constants.MajorRecordHeader(stream.RemainingMemory); + var formKey = FormKey.Factory(package.MetaData.MasterReferences, majorMeta.FormID, reference: false); + throw new ModGroupsMalformedException(package.MetaData.ModKey, $"Unexpected type encountered when parsing group locations: {majorMeta.RecordType} {formKey}"); + } + if (groupHeader.TotalLength < groupHeader.HeaderLength) + { + throw new ModGroupsMalformedException(package.MetaData.ModKey, $"Group length {groupHeader.TotalLength} is shorter than its header"); + } locations.Add(checked((int)stream.Position - offset)); - stream.Position += checked((int)meta.TotalLength); + stream.Position += checked((int)groupHeader.TotalLength); } return new GroupListOverlay( diff --git a/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/PluginBinaryOverlay.cs b/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/PluginBinaryOverlay.cs index 04b2fbda97..e2088b06fb 100644 --- a/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/PluginBinaryOverlay.cs +++ b/Mutagen.Bethesda.Core/Plugins/Binary/Overlay/PluginBinaryOverlay.cs @@ -352,6 +352,10 @@ public static IReadOnlyList ParseRecordLocations( var varMeta = stream.MetaData.Constants.GetVariableHeader(stream, constants); var recType = translationParams.ConvertToStandard(varMeta.RecordType); if (recType != trigger) break; + if (varMeta.TotalLength < varMeta.HeaderLength) + { + throw new ModGroupsMalformedException(stream.MetaData.ModKey, $"Group length {varMeta.TotalLength} is shorter than its header"); + } if (skipHeader) { stream.Position += varMeta.HeaderLength; diff --git a/Mutagen.Bethesda.Fallout3/Records/Fallout3ListGroup.cs b/Mutagen.Bethesda.Fallout3/Records/Fallout3ListGroup.cs index 048508e45b..f385a79403 100644 --- a/Mutagen.Bethesda.Fallout3/Records/Fallout3ListGroup.cs +++ b/Mutagen.Bethesda.Fallout3/Records/Fallout3ListGroup.cs @@ -2,7 +2,6 @@ using Mutagen.Bethesda.Plugins.Binary.Overlay; using Mutagen.Bethesda.Plugins.Binary.Streams; using Mutagen.Bethesda.Plugins.Binary.Translations; -using Mutagen.Bethesda.Plugins.Meta; using Mutagen.Bethesda.Plugins.Records; using Mutagen.Bethesda.Translations.Binary; using Noggog; @@ -63,8 +62,7 @@ partial void CustomFactoryEnd( stream, _recordData, _package, - offset: offset, - objectType: ObjectType.Group); + offset: offset); } public IEnumerator GetEnumerator() => Records.GetEnumerator(); diff --git a/Mutagen.Bethesda.Fallout4/Records/Fallout4ListGroup.cs b/Mutagen.Bethesda.Fallout4/Records/Fallout4ListGroup.cs index ef9862c0e4..e950ed2d29 100644 --- a/Mutagen.Bethesda.Fallout4/Records/Fallout4ListGroup.cs +++ b/Mutagen.Bethesda.Fallout4/Records/Fallout4ListGroup.cs @@ -2,7 +2,6 @@ using Mutagen.Bethesda.Plugins.Binary.Overlay; using Mutagen.Bethesda.Plugins.Binary.Streams; using Mutagen.Bethesda.Plugins.Binary.Translations; -using Mutagen.Bethesda.Plugins.Meta; using Mutagen.Bethesda.Plugins.Records; using Mutagen.Bethesda.Translations.Binary; using Noggog; @@ -63,8 +62,7 @@ partial void CustomFactoryEnd( stream, _recordData, _package, - offset: offset, - objectType: ObjectType.Group); + offset: offset); } public IEnumerator GetEnumerator() => Records.GetEnumerator(); diff --git a/Mutagen.Bethesda.Oblivion/Records/OblivionListGroup.cs b/Mutagen.Bethesda.Oblivion/Records/OblivionListGroup.cs index 58c663137b..bb624081c0 100644 --- a/Mutagen.Bethesda.Oblivion/Records/OblivionListGroup.cs +++ b/Mutagen.Bethesda.Oblivion/Records/OblivionListGroup.cs @@ -2,7 +2,6 @@ using Mutagen.Bethesda.Plugins.Binary.Overlay; using Mutagen.Bethesda.Plugins.Binary.Streams; using Mutagen.Bethesda.Plugins.Binary.Translations; -using Mutagen.Bethesda.Plugins.Meta; using Mutagen.Bethesda.Plugins.Records; using Mutagen.Bethesda.Translations.Binary; using Noggog; @@ -63,8 +62,7 @@ partial void CustomFactoryEnd( stream, _recordData, _package, - offset: offset, - objectType: ObjectType.Group); + offset: offset); } public IEnumerator GetEnumerator() => Records.GetEnumerator(); diff --git a/Mutagen.Bethesda.Skyrim/Records/ListGroup.cs b/Mutagen.Bethesda.Skyrim/Records/ListGroup.cs index d4f8b56173..2b871d3faf 100644 --- a/Mutagen.Bethesda.Skyrim/Records/ListGroup.cs +++ b/Mutagen.Bethesda.Skyrim/Records/ListGroup.cs @@ -2,7 +2,6 @@ using Mutagen.Bethesda.Plugins.Binary.Overlay; using Mutagen.Bethesda.Plugins.Binary.Streams; using Mutagen.Bethesda.Plugins.Binary.Translations; -using Mutagen.Bethesda.Plugins.Meta; using Mutagen.Bethesda.Plugins.Records; using Mutagen.Bethesda.Translations.Binary; using Noggog; @@ -64,8 +63,7 @@ partial void CustomFactoryEnd( stream, _recordData, _package, - offset: offset, - objectType: ObjectType.Group); + offset: offset); } public IEnumerator GetEnumerator() => Records.GetEnumerator(); diff --git a/Mutagen.Bethesda.Starfield/Records/StarfieldListGroup.cs b/Mutagen.Bethesda.Starfield/Records/StarfieldListGroup.cs index 76d6eaeb99..3126d21e72 100644 --- a/Mutagen.Bethesda.Starfield/Records/StarfieldListGroup.cs +++ b/Mutagen.Bethesda.Starfield/Records/StarfieldListGroup.cs @@ -2,7 +2,6 @@ using Mutagen.Bethesda.Plugins.Binary.Overlay; using Mutagen.Bethesda.Plugins.Binary.Streams; using Mutagen.Bethesda.Plugins.Binary.Translations; -using Mutagen.Bethesda.Plugins.Meta; using Mutagen.Bethesda.Plugins.Records; using Mutagen.Bethesda.Translations.Binary; using Noggog; @@ -63,8 +62,7 @@ partial void CustomFactoryEnd( stream, _recordData, _package, - offset: offset, - objectType: ObjectType.Group); + offset: offset); } public IEnumerator GetEnumerator() => Records.GetEnumerator();