/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using Asm65; using CommonUtil; namespace SourceGen.WpfGui { /// /// Split-address table generator. /// public partial class FormatAddressTable : Window, INotifyPropertyChanged { /// Format descriptors to apply. /// public SortedList NewFormatDescriptors { get; private set; } /// /// User labels to apply. /// public Dictionary NewUserLabels { get; private set; } /// /// All target offsets found. The list may contain redundant entries. /// public List AllTargetOffsets { get; private set; } /// /// If set, targets are offset by one for RTS/RTL. /// public bool IsAdjustedForReturn { get { return mIsAdjustedForReturn; } set { mIsAdjustedForReturn = value; OnPropertyChanged(); UpdateControls(); } } private bool mIsAdjustedForReturn; /// /// If set, this is a split-address table, e.g. all of the low bytes are followed /// by all of the high bytes. /// public bool IsSplitTable { get { return mIsSplitTable; } set { mIsSplitTable = value; OnPropertyChanged(); UpdateControls(); } } private bool mIsSplitTable; /// /// If set, caller will add code start points to targets. /// public bool WantCodeStartPoints { get { return mWantCodeStartPoints; } set { mWantCodeStartPoints = value; OnPropertyChanged(); } } private bool mWantCodeStartPoints; /// /// Set to true to make the "incompatible with selection" message visible. /// public bool IncompatibleSelectionVisibility { get { return mIncompatibleSelectionVisibility; } set { mIncompatibleSelectionVisibility = value; OnPropertyChanged(); } } private bool mIncompatibleSelectionVisibility; /// /// Set to true to make the "invalid constant" message visible. /// public bool InvalidConstantVisibility { get { return mInvalidConstantVisibility; } set { mInvalidConstantVisibility = value; OnPropertyChanged(); } } private bool mInvalidConstantVisibility; /// /// Set to true when valid output is available. /// private bool mOutputReady; /// /// Set to true when input is valid. Controls whether the OK button is enabled. /// public bool IsValid { get { return mIsValid; } set { mIsValid = value; OnPropertyChanged(); } } private bool mIsValid; public class OutputPreviewItem { public string Addr { get; private set; } public string Offset { get; private set; } public string Symbol { get; private set; } public OutputPreviewItem(string addr, string offset, string symbol) { Addr = addr; Offset = offset; Symbol = symbol; } } public ObservableCollection OutputPreviewList { get; private set; } /// /// Selected offsets. An otherwise contiguous range of offsets can be broken up /// by user-specified labels and address discontinuities, so this needs to be /// processed by range. /// private TypedRangeSet mSelection; /// /// Project reference. /// private DisasmProject mProject; /// /// Formatter to use when displaying addresses and hex values. /// private Formatter mFormatter; /// /// Reentrancy block for UpdateControls(). /// private bool mUpdating; // INotifyPropertyChanged implementation public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public FormatAddressTable(Window owner, DisasmProject project, TypedRangeSet selection, Formatter formatter) { InitializeComponent(); Owner = owner; DataContext = this; mProject = project; mFormatter = formatter; mSelection = selection; IsValid = false; OutputPreviewList = new ObservableCollection(); } private void Window_Loaded(object sender, RoutedEventArgs e) { mUpdating = true; string fmt, infoStr; if (mSelection.RangeCount == 1 && mSelection.Count == 1) { infoStr = (string)FindResource("str_SingleByte"); } else if (mSelection.RangeCount == 1) { fmt = (string)FindResource("str_SingleGroup"); infoStr = string.Format(fmt, mSelection.Count); } else { fmt = (string)FindResource("str_MultiGroup"); infoStr = string.Format(fmt, mSelection.Count, mSelection.RangeCount); } selectionInfoLabel.Text = infoStr; width16Radio.IsChecked = true; lowFirstPartRadio.IsChecked = true; highSecondPartRadio.IsChecked = true; bankNthPartRadio.IsChecked = true; IncompatibleSelectionVisibility = InvalidConstantVisibility = false; if (mProject.CpuDef.HasAddr16) { // Disable the 24-bit option. Having 16-bit selected will disable the rest. width24Radio.IsEnabled = false; } mUpdating = false; UpdateControls(); } private void OkButton_Click(object sender, RoutedEventArgs e) { DialogResult = true; } private void UpdateControls() { if (mUpdating) { return; } mUpdating = true; // no re-entry // handled with XAML bindings //lowThirdPartRadio.Enabled = width24Radio.Checked; //highThirdPartRadio.Enabled = width24Radio.Checked; //bankByteGroupBox.Enabled = width24Radio.Checked; lowSecondPartRadio.IsEnabled = true; // If the user selects "constant" for high byte or bank byte, then there is no // 3rd part available for low/high, so we need to turn those back off. if (width24Radio.IsChecked == true) { bool haveThree = !(highConstantRadio.IsChecked == true || bankConstantRadio.IsChecked == true); lowThirdPartRadio.IsEnabled = haveThree; highThirdPartRadio.IsEnabled = haveThree; // If "constant" is selected for high byte *and* bank byte, then there's no // 2nd part available for low. if (highConstantRadio.IsChecked == true && bankConstantRadio.IsChecked == true) { lowSecondPartRadio.IsEnabled = false; } } else { // For 16-bit address, if high byte is constant, then there's no second // part for the low byte. if (highConstantRadio.IsChecked == true) { lowSecondPartRadio.IsEnabled = false; } } // Was a now-invalidated radio button selected before? if (!lowThirdPartRadio.IsEnabled && lowThirdPartRadio.IsChecked == true) { // low now invalid, switch to whatever high isn't using if (highFirstPartRadio.IsChecked == true) { lowSecondPartRadio.IsChecked = true; } else { lowFirstPartRadio.IsChecked = true; } } if (width16Radio.IsChecked == true && highThirdPartRadio.IsChecked == true) { // high now invalid, switch to whatever low isn't using if (lowFirstPartRadio.IsChecked == true) { highSecondPartRadio.IsChecked = true; } else { highFirstPartRadio.IsChecked = true; } } if (!lowSecondPartRadio.IsEnabled && lowSecondPartRadio.IsChecked == true) { // Should only happen when high part is constant. Debug.Assert(highFirstPartRadio.IsChecked == false); lowFirstPartRadio.IsChecked = true; } mUpdating = false; UpdatePreview(); IsValid = mOutputReady; } private void WidthRadio_CheckedChanged(object sender, RoutedEventArgs e) { UpdateControls(); } private void LowByte_CheckedChanged(object sender, RoutedEventArgs e) { // If we conflict with the high byte, change the high byte. if (lowFirstPartRadio.IsChecked == true && highFirstPartRadio.IsChecked == true) { highSecondPartRadio.IsChecked = true; } else if (lowSecondPartRadio.IsChecked == true && highSecondPartRadio.IsChecked == true) { highFirstPartRadio.IsChecked = true; } else if (lowThirdPartRadio.IsChecked == true && highThirdPartRadio.IsChecked == true) { highFirstPartRadio.IsChecked = true; } UpdateControls(); } private void HighByte_CheckedChanged(object sender, RoutedEventArgs e) { // If we conflict with the low byte, change the low byte. if (lowFirstPartRadio.IsChecked == true && highFirstPartRadio.IsChecked == true) { lowSecondPartRadio.IsChecked = true; } else if (lowSecondPartRadio.IsChecked == true && highSecondPartRadio.IsChecked == true) { lowFirstPartRadio.IsChecked = true; } else if (lowThirdPartRadio.IsChecked == true && highThirdPartRadio.IsChecked == true) { lowFirstPartRadio.IsChecked = true; } UpdateControls(); } private void BankByte_CheckedChanged(object sender, EventArgs e) { UpdateControls(); } private void HighConstantTextBox_TextChanged(object sender, TextChangedEventArgs e) { highConstantRadio.IsChecked = true; UpdateControls(); } private void BankConstantTextBox_TextChanged(object sender, TextChangedEventArgs e) { bankConstantRadio.IsChecked = true; UpdateControls(); } private void UpdatePreview() { mOutputReady = false; int minDiv; if (width16Radio.IsChecked == true) { if (highConstantRadio.IsChecked == true) { minDiv = 1; } else { minDiv = 2; } } else { if (highConstantRadio.IsChecked == true) { if (bankConstantRadio.IsChecked == true) { minDiv = 1; } else { minDiv = 2; } } else { if (bankConstantRadio.IsChecked == true) { minDiv = 2; } else { minDiv = 3; } } } IncompatibleSelectionVisibility = InvalidConstantVisibility = false; // Start by clearing the previous contents of the list. If something goes // wrong, we want to show the error messages on an empty list. OutputPreviewList.Clear(); if ((mSelection.Count % minDiv) != 0) { IncompatibleSelectionVisibility = true; return; } int highConstant = -1; if (highConstantRadio.IsChecked == true) { if (!Number.TryParseInt(highConstantTextBox.Text, out highConstant, out int unused) || (highConstant != (byte) highConstant)) { InvalidConstantVisibility = true; return; } } int bankConstant = -1; if (bankConstantRadio.IsEnabled && bankConstantRadio.IsChecked == true) { if (!Number.TryParseInt(bankConstantTextBox.Text, out bankConstant, out int unused) || (bankConstant != (byte) bankConstant)) { InvalidConstantVisibility = true; return; } } // Looks valid, generate format list. GenerateFormats(minDiv, highConstant, bankConstant); } private void GenerateFormats(int div, int highConst, int bankConst) { SortedList newDfds = new SortedList(); Dictionary newLabels = new Dictionary(); List targetOffsets = new List(); bool isBigEndian; // Identify the offset where each set of data starts. int span = mSelection.Count / div; int lowOff, highOff, bankOff; int stride; if (lowFirstPartRadio.IsChecked == true) { lowOff = 0; isBigEndian = false; } else if (lowSecondPartRadio.IsChecked == true) { lowOff = 1; isBigEndian = true; } else if (lowThirdPartRadio.IsChecked == true) { lowOff = 2; isBigEndian = true; } else { Debug.Assert(false); lowOff = -1; isBigEndian = false; } if (highFirstPartRadio.IsChecked == true) { highOff = 0; } else if (highSecondPartRadio.IsChecked == true) { highOff = 1; } else if (highThirdPartRadio.IsChecked == true) { highOff = 2; } else { highOff = -1; // use constant } if (width24Radio.IsChecked == true) { if (bankNthPartRadio.IsChecked == true) { // Use whichever part isn't being used by the other two. if (lowOff != 0 && highOff != 0) { bankOff = 0; } else if (lowOff != 1 && highOff != 1) { bankOff = 1; } else { Debug.Assert(lowOff != 2 && highOff != 2); bankOff = 2; } } else { bankOff = -1; // use constant } } else { bankOff = -1; // use constant // Set the constant bank value equal to whichever bank the table lives in. // This is correct for "JMP (addr,X)". We use the offset of the first item // in the range set, which is a little awkward to get to. int offset = 0; foreach (TypedRangeSet.Tuple tup in mSelection) { offset = tup.Value; break; } int addr = mProject.AddrMap.OffsetToAddress(offset); bankConst = (byte)(addr >> 16); } if (IsSplitTable) { // Split table, so stride is 1 and each section start is determined by the span. stride = 1; lowOff *= span; highOff *= span; bankOff *= span; } else { // For non-split table, the stride is the width of each entry. stride = 1; if (highOff >= 0) { stride++; } if (bankOff >= 0) { stride++; } } Debug.WriteLine("FormatAddressTable: stride=" + stride + " span=" + span + " count=" + mSelection.Count); Debug.WriteLine(" low=" + lowOff + " high=" + highOff + " bank=" + bankOff); // The TypedRangeSet doesn't have an index operation, so copy the values into // an array. int[] offsets = new int[mSelection.Count]; int index = 0; foreach (TypedRangeSet.Tuple tup in mSelection) { offsets[index++] = tup.Value; } int adj = 0; if (IsAdjustedForReturn) { adj = 1; } // Walk through the file data, generating addresses as we go. byte[] fileData = mProject.FileData; for (int i = 0; i < span; i++) { byte low, high, bank; low = fileData[offsets[lowOff + i * stride]]; if (highOff >= 0) { high = fileData[offsets[highOff + i * stride]]; } else { high = (byte) highConst; } if (bankOff >= 0) { bank = fileData[offsets[bankOff + i * stride]]; } else { bank = (byte) bankConst; } int addr = ((bank << 16) | (high << 8) | low) + adj; int targetOffset = mProject.AddrMap.AddressToOffset(offsets[0], addr); if (targetOffset < 0) { // Address not within file bounds. // TODO(maybe): look for matching platform/project symbols AddPreviewItem(addr, -1, Res.Strings.INVALID_ADDRESS); } else { // Note the same target offset may appear more than once. targetOffsets.Add(targetOffset); // If there's a user-defined label there already, use it. Otherwise, we'll // need to generate one. string targetLabel; if (mProject.UserLabels.TryGetValue(targetOffset, out Symbol sym)) { targetLabel = sym.Label; AddPreviewItem(addr, targetOffset, targetLabel); } else { // Generate a symbol that's unique vs. the symbol table. We don't need // it to be unique vs. the labels we're generating here, because we // won't generate identical labels for different addresses, and we do // want to generate a single label if more than one table entry refers // to the same target. Symbol tmpSym = AutoLabel.GenerateUniqueForAddress(addr, mProject.SymbolTable, "T"); // tmpSym was returned as an auto-label, make it a user label instead // (with global scope) tmpSym = new Symbol(tmpSym.Label, tmpSym.Value, Symbol.Source.User, Symbol.Type.GlobalAddr, Symbol.LabelAnnotation.Generated); newLabels[targetOffset] = tmpSym; // overwrites previous targetLabel = tmpSym.Label; AddPreviewItem(addr, targetOffset, "(+) " + targetLabel); } if (IsSplitTable) { // Now we need to create format descriptors for the addresses where we // extracted the low, high, and bank values. newDfds.Add(offsets[lowOff + i * stride], FormatDescriptor.Create(1, new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.Low), false)); if (highOff >= 0) { newDfds.Add(offsets[highOff + i * stride], FormatDescriptor.Create(1, new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.High), false)); } if (bankOff >= 0) { newDfds.Add(offsets[bankOff + i * stride], FormatDescriptor.Create(1, new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.Bank), false)); } } else { // Create a single format descriptor that spans all bytes. Note we // don't want to use lowOff here -- we want to put the format on // whichever byte came first. // TODO(maybe): we don't correctly deal with a "scrambled" non-split // 24-bit table, i.e. low then bank then high. This is not really // a thing, but we should either prevent it or punt to single-byte // like we do for split tables. Debug.Assert(stride >= 1 && stride <= 3); newDfds.Add(offsets[0 + i * stride], FormatDescriptor.Create(stride, new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.Low), isBigEndian)); } } } NewFormatDescriptors = newDfds; NewUserLabels = newLabels; AllTargetOffsets = targetOffsets; // Don't show ready if all addresses are invalid. It's okay if some work and // some don't. mOutputReady = (AllTargetOffsets.Count > 0); } private void AddPreviewItem(int addr, int offset, string label) { OutputPreviewItem newItem = new OutputPreviewItem( mFormatter.FormatAddress(addr, !mProject.CpuDef.HasAddr16), (offset >= 0 ? mFormatter.FormatOffset24(offset) : "---"), label); OutputPreviewList.Add(newItem); } } }