diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index e5e5fe07..bfc08ab3 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -2445,6 +2445,8 @@ namespace HavenSoft.HexManiac.Core.Models { return new ErrorInfo("An existing anchor starts before the new one ends."); } else if (!name.All(c => char.IsLetterOrDigit(c) || "-._".Contains(c))) { // at this point, the name might have a "-1" on the end, so still allow the dash return new ErrorInfo("Anchor names must contain only letters, numbers, dots, and underscores."); + } else if (runToWrite.Start + runToWrite.Length > model.Count) { + return new ErrorInfo("Anchor format must not go past the end of the file."); } else { return ErrorInfo.NoError; } diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index f443c22d..b87438bd 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -528,6 +528,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { ElementContent = ParseSegments(segments, data); if (ElementContent.Count == 0) throw new ArrayRunParseException("Array Content must not be empty."); ElementLength = ElementContent.Sum(e => e.Length); + if (ElementLength == 0) throw new ArrayRunParseException("Array Content Length must not be zero."); FormatMatchFlags flags = default; if (ElementContent.Count == 1) flags |= FormatMatchFlags.IsSingleSegment; @@ -1243,6 +1244,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { segments = segments.Substring(nameEnd); var (format, formatLength, segmentLength) = ExtractSingleFormat(segments, model); if (name == string.Empty && format != ElementContentType.Splitter) throw new ArrayRunParseException("expected name, but none was found: " + segments); + if (format == ElementContentType.PCS && segmentLength < 1) throw new ArrayRunParseException("Cannot have 0-length text: " + name); // check to see if a name or length is part of the format if (format == ElementContentType.Integer && segments.Length > formatLength && segments[formatLength] != ' ') { diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index fcb22881..e0eb3ea6 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -2601,7 +2601,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { // normal case: whether or not to accept the edit depends on the existing cell format var dataIndex = scroll.ViewPointToDataIndex(point); - var completeEditOperation = new CompleteCellEdit(Model, scroll, dataIndex, underEdit.CurrentText, history.CurrentChange); + var completeEditOperation = new CompleteCellEdit(Model, scroll, dataIndex, underEdit.CurrentText, () => history.CurrentChange); using (ModelCacheScope.CreateScope(Model)) { (underEdit.OriginalFormat ?? Undefined.Instance).Visit(completeEditOperation, element.Value); diff --git a/src/HexManiac.Core/ViewModels/Visitors/AutocompleteCell.cs b/src/HexManiac.Core/ViewModels/Visitors/AutocompleteCell.cs index 354cc254..57ad00c8 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/AutocompleteCell.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/AutocompleteCell.cs @@ -113,7 +113,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { private void GenerateOptions(IDataFormatInstance format) { var arrayRun = (ITableRun)Model.GetNextRun(format.Source); var offsets = arrayRun.ConvertByteOffsetToArrayOffset(format.Source); - var segment = (IHasOptions)arrayRun.ElementContent[offsets.SegmentIndex]; + var contentSegment = arrayRun.ElementContent[offsets.SegmentIndex]; + if (contentSegment is ArrayRunRecordSegment record) contentSegment = record.CreateConcrete(Model, format.Source); + var segment = (IHasOptions)contentSegment; var allOptions = segment.GetOptions(Model).Where(option => option != null).Select(option => option + " "); Result = AutoCompleteSelectionItem.Generate(allOptions.Where(option => option.MatchesPartial(InputText, onlyCheckLettersAndDigits: true)), -1); } diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteCellEdit.cs index 229f8422..0ca6c5d5 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteCellEdit.cs @@ -15,7 +15,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { private readonly IDataModel Model; private readonly int memoryLocation; private readonly string CurrentText; - private readonly ModelDelta CurrentChange; + private readonly Func currentChange; private readonly ScrollRegion scroll; public bool Result { get; private set; } // if true, the edit was completed correctly @@ -27,12 +27,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { // and refresh the one cell (along with any other UnderEdit cells) // if result is true and this _is_ null, then the entire screen needs to be refreshed. - public CompleteCellEdit(IDataModel model, ScrollRegion scroll, int memoryLocation, string currentText, ModelDelta currentChange) { + private ModelDelta CurrentChange => currentChange(); + + public CompleteCellEdit(IDataModel model, ScrollRegion scroll, int memoryLocation, string currentText, Func currentChange) { Model = model; this.scroll = scroll; this.memoryLocation = memoryLocation; CurrentText = currentText; - CurrentChange = currentChange; + this.currentChange = currentChange; NewDataIndex = memoryLocation; } @@ -585,7 +587,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { string sanitizedText = CurrentText.Replace(')', ' '); var array = (ITableRun)Model.GetNextRun(memoryLocation); var offsets = array.ConvertByteOffsetToArrayOffset(memoryLocation); - var segment = (ArrayRunEnumSegment)array.ElementContent[offsets.SegmentIndex]; + var contentSegment = array.ElementContent[offsets.SegmentIndex]; + if (contentSegment is ArrayRunRecordSegment record) contentSegment = record.CreateConcrete(Model, memoryLocation); + var segment = (ArrayRunEnumSegment)contentSegment; if (segment.TryParse(Model, sanitizedText, out int value)) { Model.WriteMultiByteValue(offsets.SegmentStart, segment.Length, CurrentChange, value); NewDataIndex = offsets.SegmentStart + segment.Length; diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs index a6c7219f..17209192 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs @@ -98,7 +98,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(IntegerEnum integer, byte data) { Result = integer.CanStartWithCharacter(Input) || - ".'~|,_&%)".Contains(Input) || + ".'~|,_&%()".Contains(Input) || char.IsWhiteSpace(Input); } @@ -137,7 +137,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } public void Visit(BitArray array, byte data) { - Result = char.IsLetterOrDigit(Input) || Input.IsAny('"', '-', ' '); + Result = char.IsLetterOrDigit(Input) || "?-\" .'~|,_&%)".Contains(Input); } public void Visit(MatchedWord word, byte data) => Visit((None)null, data); diff --git a/src/HexManiac.Tests/Before_Baseclass/PointerModelTests.cs b/src/HexManiac.Tests/Before_Baseclass/PointerModelTests.cs index b2894e31..210407c6 100644 --- a/src/HexManiac.Tests/Before_Baseclass/PointerModelTests.cs +++ b/src/HexManiac.Tests/Before_Baseclass/PointerModelTests.cs @@ -980,6 +980,13 @@ namespace HavenSoft.HexManiac.Tests { Assert.Empty(Errors); } + [Fact] + public void NoChanges_StartChangeThenCancel_NoSaveOption() { + ViewPort.Edit("<"); + + Assert.False(ViewPort.Save.CanExecute(FileSystem)); + } + private void StandardSetup(out byte[] data, out PokemonModel model, out ViewPort viewPort) { data = new byte[0x200]; model = new PokemonModel(data); diff --git a/src/HexManiac.Tests/Before_Baseclass/StringModelTests.cs b/src/HexManiac.Tests/Before_Baseclass/StringModelTests.cs index 54d5d531..43e5d4d3 100644 --- a/src/HexManiac.Tests/Before_Baseclass/StringModelTests.cs +++ b/src/HexManiac.Tests/Before_Baseclass/StringModelTests.cs @@ -757,6 +757,18 @@ namespace HavenSoft.HexManiac.Tests { Assert.IsAssignableFrom(Model.GetNextRun(0)); } + [Fact] + public void TableWithZeroLengthString_Error() { + ViewPort.Edit("^table[text\"\"0]1 "); + Assert.Single(Errors); + } + + [Fact] + public void Table_OnlyZeroLengthSegments_Error() { + ViewPort.Edit("^table[field|=() ]1 "); + Assert.Single(Errors); + } + private void HackTextConverter(string game) { var converter = new PCSConverter(game); var property = Model.GetType().GetProperty(nameof(Model.TextConverter)); diff --git a/src/HexManiac.Tests/Before_Baseclass/ViewPortScrollTests.cs b/src/HexManiac.Tests/Before_Baseclass/ViewPortScrollTests.cs index c42e57e3..666d7f1a 100644 --- a/src/HexManiac.Tests/Before_Baseclass/ViewPortScrollTests.cs +++ b/src/HexManiac.Tests/Before_Baseclass/ViewPortScrollTests.cs @@ -357,5 +357,15 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal(10, ViewPort.DataLength); } + + [Theory] + [InlineData("^table[a: b: c: d: e:]3 ")] + [InlineData("^text`asc`20 ")] + public void EndOfModel_LongRunFormat_Error(string format) { + ViewPort.Goto.Execute(0x1F0); + ViewPort.Edit(format); + Assert.Single(Errors); + } } + } diff --git a/src/HexManiac.Tests/ListTests.cs b/src/HexManiac.Tests/ListTests.cs index 3c61427b..c88eb38b 100644 --- a/src/HexManiac.Tests/ListTests.cs +++ b/src/HexManiac.Tests/ListTests.cs @@ -1,6 +1,7 @@ using HavenSoft.HexManiac.Core; using HavenSoft.HexManiac.Core.Models; using HavenSoft.HexManiac.Core.Models.Runs; +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using HavenSoft.HexManiac.Core.ViewModels.Tools; using HavenSoft.HexManiac.Core.ViewModels.Visitors; using System; @@ -317,5 +318,15 @@ DefaultHash = '''0BEEDA92''' Assert.Equal("name2", list[2]); Assert.Equal(3, list.Count); } + + [Fact] + public void ListWithApostrophe_TypeIntoBitFieldFromList_AcceptsApostrophe() { + Model.SetList("list", new[] { "Item", "Arby's", "Content" }); + ViewPort.Edit("^table[content|b[]list]1 "); + + ViewPort.Edit("arby'"); + + Assert.IsType(ViewPort[0, 0].Format); + } } } diff --git a/src/HexManiac.Tests/TableTests.cs b/src/HexManiac.Tests/TableTests.cs index cb262d23..5d9896c3 100644 --- a/src/HexManiac.Tests/TableTests.cs +++ b/src/HexManiac.Tests/TableTests.cs @@ -1322,6 +1322,16 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal(0x108, ViewPort.ConvertViewPointToAddress(ViewPort.SelectionStart)); } + [Fact] + public void SwitchFormat_EditEnum_NoError() { + CreateTextTable("names", 0x100, "adam", "bob", "carl", "dave"); + ViewPort.Edit("@000 ^table[input: arg:|s=input(0=names)]2 "); + + ViewPort.Edit("@002 carl "); + + Assert.Equal(2, Model[2]); + } + private void ArrangeTrainerPokemonTeamData(byte structType, byte pokemonCount, int trainerCount) { CreateTextTable(HardcodeTablesModel.PokemonNameTable, 0x180, "ABCDEFGHIJKLMNOP".Select(c => c.ToString()).ToArray()); CreateTextTable(HardcodeTablesModel.MoveNamesTable, 0x1B0, "qrstuvwxyz".Select(c => c.ToString()).ToArray());