/*
* 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.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using Asm65;
using CommonUtil;
using TextScanMode = SourceGen.ProjectProperties.AnalysisParameters.TextScanMode;
namespace SourceGen.WpfGui {
///
/// Data operand editor.
///
public partial class EditDataOperand : Window, INotifyPropertyChanged {
///
/// Result set that describes the formatting to perform. Not all regions will have
/// the same format, e.g. the "mixed ASCII" mode will alternate strings and bytes
/// (rather than a dedicated "mixed ASCII" format type).
///
public SortedList Results { 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;
///
/// FormatDescriptor from the first offset. May be null if the offset doesn't
/// have a format descriptor specified. This will be used to configure the
/// dialog controls if the format is suited to the selection. The goal is to
/// make single-item editing work as expected.
///
public FormatDescriptor mFirstFormatDescriptor;
///
/// Project reference.
///
private DisasmProject mProject;
///
/// Raw file data.
///
private byte[] mFileData;
///
/// Symbol table to use when resolving symbolic values.
///
private SymbolTable mSymbolTable;
///
/// Map of offsets to addresses.
///
private AddressMap mAddrMap;
///
/// Formatter to use when displaying addresses and hex values.
///
private Asm65.Formatter mFormatter;
///
/// Set to true if, during the initial setup, the format defined by FirstFormatDescriptor
/// was unavailable.
///
private bool mPreferredFormatUnavailable;
///
/// 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 int MaxDenseBytesPerLine {
get { return mMaxDenseBytesPerLine; }
set {
if (mMaxDenseBytesPerLine != value) {
mMaxDenseBytesPerLine = value;
OnPropertyChanged();
UpdateControls();
if (IsLoaded) {
// Set the radio button when text is typed.
radioDenseHexLimited.IsChecked = true;
}
}
}
}
private int mMaxDenseBytesPerLine;
///
/// Text encoding combo box item. We use the same TextScanMode enum that the
/// uncategorized data analyzer uses.
///
public class StringEncodingItem {
public string Name { get; private set; }
public TextScanMode Mode { get; private set; }
public StringEncodingItem(string name, TextScanMode mode) {
Name = name;
Mode = mode;
}
}
public StringEncodingItem[] StringEncodingItems { get; private set; }
public class JunkAlignmentItem {
public string Description { get; private set; }
public FormatDescriptor.SubType FormatSubType { get; private set; }
public JunkAlignmentItem(string descr, FormatDescriptor.SubType subFmt) {
Description = descr;
FormatSubType = subFmt;
}
}
public List JunkAlignmentItems { get; private set; }
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public EditDataOperand(Window owner, DisasmProject project,
Asm65.Formatter formatter, TypedRangeSet trs, FormatDescriptor firstDesc) {
InitializeComponent();
Owner = owner;
DataContext = this;
mProject = project;
mFileData = project.FileData;
mSymbolTable = project.SymbolTable;
mAddrMap = project.AddrMap;
mFormatter = formatter;
mSelection = trs;
mFirstFormatDescriptor = firstDesc;
MaxDenseBytesPerLine = AppSettings.Global.GetInt(AppSettings.OPED_DENSE_HEX_LIMIT, 8);
StringEncodingItems = new StringEncodingItem[] {
new StringEncodingItem(Res.Strings.SCAN_LOW_ASCII,
TextScanMode.LowAscii),
new StringEncodingItem(Res.Strings.SCAN_LOW_HIGH_ASCII,
TextScanMode.LowHighAscii),
new StringEncodingItem(Res.Strings.SCAN_C64_PETSCII,
TextScanMode.C64Petscii),
new StringEncodingItem(Res.Strings.SCAN_C64_SCREEN_CODE,
TextScanMode.C64ScreenCode),
};
GetMinMaxAlignment(out FormatDescriptor.SubType min, out FormatDescriptor.SubType max);
//Debug.WriteLine("ALIGN: min=" + min + " max=" + max);
Debug.Assert(min == FormatDescriptor.SubType.None ^ // both or neither are None
max != FormatDescriptor.SubType.None);
int junkSel = 0;
string noAlign = (string)FindResource("str_AlignmentNone");
string alignFmt = (string)FindResource("str_AlignmentItemFmt");
JunkAlignmentItems = new List();
JunkAlignmentItems.Add(new JunkAlignmentItem(noAlign, FormatDescriptor.SubType.None));
if (min != FormatDescriptor.SubType.None) {
int index = 1;
// We assume the enum values are consecutive and ascending.
FormatDescriptor.SubType end = (FormatDescriptor.SubType)(((int)max) + 1);
while (min != end) {
int pwr = FormatDescriptor.AlignmentToPower(min);
string endStr = mFormatter.FormatHexValue(1 << pwr, 4);
JunkAlignmentItems.Add(new JunkAlignmentItem(
string.Format(alignFmt, 1 << pwr, endStr), min));
// See if this matches previous value.
if (mFirstFormatDescriptor != null &&
mFirstFormatDescriptor.FormatType == FormatDescriptor.Type.Junk &&
mFirstFormatDescriptor.FormatSubType == min) {
junkSel = index;
}
// Advance.
min = (FormatDescriptor.SubType)(((int)min) + 1);
index++;
}
}
junkAlignComboBox.SelectedIndex = junkSel;
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
DateTime startWhen = DateTime.Now;
// Determine which of the various options is suitable for the selected offsets.
// Disable any radio buttons that won't work.
AnalyzeRanges();
// This gets invoked a bit later, from the "selection changed" callback.
//AnalyzeStringRanges(TextScanMode.LowHighAscii);
// Configure the dialog from the FormatDescriptor, if one is available.
Debug.WriteLine("First FD: " + mFirstFormatDescriptor);
SetControlsFromDescriptor(mFirstFormatDescriptor);
if (mPreferredFormatUnavailable) {
// This can happen when e.g. a bunch of stuff is formatted as null-terminated
// strings. We don't recognize a lone zero as a string, but we allow it if
// it's next to a bunch of others. If you come back later and try to format
// just that one byte, you end up here.
// TODO(maybe): make it more obvious what's going on?
Debug.WriteLine("NOTE: preferred format unavailable");
}
UpdateControls();
Debug.WriteLine("EditData dialog load time: " +
(DateTime.Now - startWhen).TotalMilliseconds + " ms");
}
private void Window_ContentRendered(object sender, EventArgs e) {
// Start with the focus in the text box if the initial format allows for a
// symbolic reference. This way they can start typing immediately.
if (simpleDisplayAsGroupBox.IsEnabled) {
symbolEntryTextBox.Focus();
}
}
///
/// Handles Checked event for all buttons in Main group.
///
private void MainGroup_CheckedChanged(object sender, EventArgs e) {
// Enable/disable the style group and the low/high/bank radio group.
// Update preview window.
UpdateControls();
}
///
/// Handles Checked event for radio buttons in the Display group.
/// group box.
///
private void SimpleDisplay_CheckedChanged(object sender, EventArgs e) {
// Enable/disable the low/high/bank radio group.
UpdateControls();
}
private void SymbolEntryTextBox_TextChanged(object sender, TextChangedEventArgs e) {
// Make sure Symbol is checked if they're typing text in.
//Debug.Assert(radioSimpleDataSymbolic.IsEnabled);
radioSimpleDataSymbolic.IsChecked = true;
// Update OK button based on symbol validity.
UpdateControls();
}
///
/// Sets the string encoding combo box to an item that matches the specified mode. If
/// the mode can't be found, an arbitrary entry will be chosen.
///
private void SetStringEncoding(TextScanMode mode) {
StringEncodingItem choice = null;
foreach (StringEncodingItem item in StringEncodingItems) {
if (item.Mode == mode) {
choice = item;
break;
}
}
if (choice == null) {
choice = StringEncodingItems[1];
}
stringEncodingComboBox.SelectedItem = choice;
}
private void StringEncodingComboBox_SelectionChanged(object sender,
SelectionChangedEventArgs e) {
if (!IsLoaded) {
return;
}
StringEncodingItem item = (StringEncodingItem)stringEncodingComboBox.SelectedItem;
AnalyzeStringRanges(item.Mode);
UpdateControls();
AppSettings.Global.SetEnum(AppSettings.OPED_DEFAULT_STRING_ENCODING,
typeof(TextScanMode), (int)item.Mode);
}
private void OkButton_Click(object sender, RoutedEventArgs e) {
CreateDescriptorListFromControls();
FormatDescriptor.DebugDumpSortedList(Results);
DialogResult = true;
}
///
/// Updates all of the controls to reflect the current internal state.
///
private void UpdateControls() {
if (!IsLoaded) {
return;
}
// Configure the simple data "display as" style box.
bool wantStyle = false;
int simpleWidth = -1;
bool isBigEndian = false;
if (radioSingleBytes.IsChecked == true) {
wantStyle = true;
simpleWidth = 1;
} else if (radio16BitLittle.IsChecked == true) {
wantStyle = true;
simpleWidth = 2;
} else if (radio16BitBig.IsChecked == true) {
wantStyle = true;
simpleWidth = 2;
isBigEndian = true;
} else if (radio24BitLittle.IsChecked == true) {
wantStyle = true;
simpleWidth = 3;
} else if (radio32BitLittle.IsChecked == true) {
wantStyle = true;
simpleWidth = 4;
}
bool focusOnSymbol = !simpleDisplayAsGroupBox.IsEnabled && wantStyle;
simpleDisplayAsGroupBox.IsEnabled = wantStyle;
if (wantStyle) {
// Because this covers multiple items in a data area, we allow the
// "extended" set, which includes some control characters.
radioSimpleDataAscii.IsEnabled = IsCompatibleWithCharSet(simpleWidth,
isBigEndian, CharEncoding.IsExtendedLowOrHighAscii);
radioSimpleDataPetscii.IsEnabled = IsCompatibleWithCharSet(simpleWidth,
isBigEndian, CharEncoding.IsExtendedC64Petscii);
radioSimpleDataScreenCode.IsEnabled = IsCompatibleWithCharSet(simpleWidth,
isBigEndian, CharEncoding.IsExtendedC64ScreenCode);
}
// Enable the symbolic reference entry box if the "display as" group is enabled.
// That way instead of "click 16-bit", "click symbol", "enter symbol", the user
// can skip the second step.
symbolEntryTextBox.IsEnabled = simpleDisplayAsGroupBox.IsEnabled;
// Part panel is enabled when Symbol is checked. (Now handled in XAML.)
//symbolPartPanel.IsEnabled = (radioSimpleDataSymbolic.IsChecked == true);
// If we just enabled the group box, set the focus on the symbol entry box. This
// removes another click from the steps, though it's a bit aggressive if you're
// trying to arrow your way through the items.
if (focusOnSymbol) {
symbolEntryTextBox.Focus();
}
// Disable the alignment pop-up unless Junk is selected.
junkAlignComboBox.IsEnabled = (radioJunk.IsChecked == true);
bool isOk = true;
if (radioSimpleDataSymbolic.IsChecked == true) {
// Just check for correct format. References to non-existent labels are allowed.
Symbol.TrimAndValidateLabel(symbolEntryTextBox.Text,
mFormatter.NonUniqueLabelPrefix, out isOk, out bool unused1,
out bool unused2, out bool unused3, out Symbol.LabelAnnotation unused4);
// Actually, let's discourage references to auto-labels and variables.
if (isOk && mSymbolTable.TryGetValue(symbolEntryTextBox.Text, out Symbol sym)) {
isOk = sym.SymbolSource != Symbol.Source.Auto &&
sym.SymbolSource != Symbol.Source.Variable;
}
}
IsValid = isOk;
// If dense hex with a limit is selected, check the value.
if (radioDenseHexLimited.IsChecked == true) {
if (MaxDenseBytesPerLine > 0) {
AppSettings.Global.SetInt(AppSettings.OPED_DENSE_HEX_LIMIT,
MaxDenseBytesPerLine);
} else {
IsValid = false;
}
}
}
#region Setup
///
/// Determines the minimum and maximum alignment values, based on the sizes of the
/// regions and the address they end on. Used for .align.
///
/// Minimum allowed format, or None.
/// Maximum allowed format, or None.
private void GetMinMaxAlignment(out FormatDescriptor.SubType min,
out FormatDescriptor.SubType max) {
min = max = FormatDescriptor.SubType.None;
int maxLenPow = -1;
int minAlignPow = 65535;
IEnumerator iter = mSelection.RangeListIterator;
while (iter.MoveNext()) {
TypedRangeSet.TypedRange rng = iter.Current;
int length = rng.High - rng.Low + 1;
Debug.Assert(length > 0);
// The goal is to find an instruction that fills an entire region with zeroes
// or junk bytes for the sole purpose of ending at a specific boundary.
//
// If we have a 100-byte region that ends at address $103f (inclusive), it
// can't be the result of an assembler alignment directive. "align $40" would
// have stopped at $1000, "align $80" would have continued on to $107f.
//
// Alignment of junk whose last byte $103f could be due to Align2, Align4 (1-3
// bytes at $103d/e/f), Align8 (1-7 bytes at $1039-f), and so on, up to Align64.
// The size of the buffer determines the minimum value, the end address
// determines the maximum.
//
// Bear in mind that assembler alignment directives will do nothing if the
// address is already aligned: Align256 at $1000 generates no output. So we
// cannot use Align8 on a buffer of length 8.
// Count the trailing 1 bits in the address. This gets us the power of 2
// alignment value. Note alignPow will be zero if the last byte is stored at
// an even address.
int endAddress = mAddrMap.OffsetToAddress(rng.High) & 0x0000ffff;
int alignPow = BitTwiddle.CountTrailingZeroes(~endAddress);
// Round length up to next highest power of 2, and compute Log2(). Unfortunately
// .NET Standard 2.0 doesn't have Math.Log2(). Note we want the next-highest
// even if it's already a power of 2.
int lenRound = BitTwiddle.NextHighestPowerOf2(length);
int lenPow = BitTwiddle.CountTrailingZeroes(lenRound);
Debug.Assert(lenPow > 0); // length==1 -> lenRound=2 --> lenPow=1
// Want the biggest minimum value and the smallest maximum value.
if (maxLenPow < lenPow) {
maxLenPow = lenPow;
}
if (minAlignPow > alignPow) {
minAlignPow = alignPow;
}
if (maxLenPow > minAlignPow) {
return;
}
}
min = FormatDescriptor.PowerToAlignment(maxLenPow);
max = FormatDescriptor.PowerToAlignment(minAlignPow);
}
///
/// Analyzes the selection to see which data formatting options are suitable.
/// Disables radio buttons and updates labels.
///
/// Call this once, when the dialog is first loaded.
///
private void AnalyzeRanges() {
Debug.Assert(mSelection.Count != 0);
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);
}
selectFormatLabel.Text = infoStr;
IEnumerator iter = mSelection.RangeListIterator;
// For each range, check to see if the data within qualifies for the various
// options. If any of them fail to meet the criteria, the option is disabled
// for all ranges.
while (iter.MoveNext()) {
TypedRangeSet.TypedRange rng = iter.Current;
Debug.WriteLine("Testing [" + rng.Low + ", " + rng.High + "]");
// Note single-byte and dense are always enabled.
int count = rng.High - rng.Low + 1;
Debug.Assert(count > 0);
if ((count & 0x01) != 0) {
// not divisible by 2, disallow 16-bit entries
radio16BitLittle.IsEnabled = false;
radio16BitBig.IsEnabled = false;
}
if ((count & 0x03) != 0) {
// not divisible by 4, disallow 32-bit entries
radio32BitLittle.IsEnabled = false;
}
if ((count / 3) * 3 != count) {
// not divisible by 3, disallow 24-bit entries
radio24BitLittle.IsEnabled = false;
}
// Check for run of bytes (2 or more of the same thing). Remember that
// we check this one region at a time, and each region could have different
// bytes, but so long as the bytes are all the same within a region we're good.
if (radioFill.IsEnabled && count > 1 &&
DataAnalysis.RecognizeRun(mFileData, rng.Low, rng.High) == count) {
// LGTM
} else {
radioFill.IsEnabled = false;
}
}
}
///
/// Analyzes the selection to see which string formatting options are suitable.
/// Disables radio buttons and updates labels.
///
/// Call this when the character encoding selection changes.
///
private void AnalyzeStringRanges(TextScanMode scanMode) {
Debug.WriteLine("Analyzing string ranges");
Debug.Assert(IsLoaded);
int mixedCharOkCount = 0;
int mixedCharNotCount = 0;
int nullTermStringCount = 0;
int len8StringCount = 0;
int len16StringCount = 0;
int dciStringCount = 0;
CharEncoding.InclusionTest charTest;
switch (scanMode) {
case TextScanMode.LowAscii:
charTest = CharEncoding.IsExtendedAscii;
break;
case TextScanMode.LowHighAscii:
charTest = CharEncoding.IsExtendedLowOrHighAscii;
break;
case TextScanMode.C64Petscii:
charTest = CharEncoding.IsExtendedC64Petscii;
break;
case TextScanMode.C64ScreenCode:
charTest = CharEncoding.IsExtendedC64ScreenCode;
break;
default:
Debug.Assert(false);
charTest = CharEncoding.IsExtendedAscii;
break;
}
radioStringMixed.IsEnabled = true;
radioStringMixedReverse.IsEnabled = true;
radioStringNullTerm.IsEnabled = (scanMode != TextScanMode.C64ScreenCode);
radioStringLen8.IsEnabled = true;
radioStringLen16.IsEnabled = true;
radioStringDci.IsEnabled = true;
IEnumerator iter = mSelection.RangeListIterator;
while (iter.MoveNext()) {
TypedRangeSet.TypedRange rng = iter.Current;
Debug.WriteLine("Testing [" + rng.Low + ", " + rng.High + "]");
// See if there's enough string data to make it worthwhile. We use an
// arbitrary threshold of 2+ printable characters, and require twice as many
// printable as non-printable.
if (radioStringMixed.IsEnabled) {
if (scanMode == TextScanMode.LowHighAscii) {
// We use a special test that counts low, high, and non-ASCII.
// Whichever form of ASCII has the highest count is the winner, and
// the loser is counted as non-ASCII.
int asciiCount;
DataAnalysis.CountHighLowBytes(mFileData, rng.Low, rng.High, charTest,
out int lowAscii, out int highAscii, out int nonAscii);
if (highAscii > lowAscii) {
asciiCount = highAscii;
nonAscii += lowAscii;
} else {
asciiCount = lowAscii;
nonAscii += highAscii;
}
if (asciiCount >= 2 && asciiCount >= nonAscii * 2) {
// Looks good
mixedCharOkCount += asciiCount;
mixedCharNotCount += nonAscii;
} else {
// Fail
radioStringMixed.IsEnabled = false;
radioStringMixedReverse.IsEnabled = false;
mixedCharOkCount = mixedCharNotCount = -1;
}
} else {
int matchCount = DataAnalysis.CountCharacterBytes(mFileData,
rng.Low, rng.High, charTest);
int missCount = (rng.High - rng.Low + 1) - matchCount;
if (matchCount >= 2 && matchCount >= missCount * 2) {
mixedCharOkCount += matchCount;
mixedCharNotCount += missCount;
} else {
// Fail
radioStringMixed.IsEnabled = false;
radioStringMixedReverse.IsEnabled = false;
mixedCharOkCount = mixedCharNotCount = -1;
}
}
}
// Check for null-terminated strings. Zero-length strings are allowed, but
// not counted -- we want to have some actual character data. Individual
// ASCII strings need to be entirely high-ASCII or low-ASCII, but not all strings
// in a region have to be the same.
if (radioStringNullTerm.IsEnabled) {
int strCount = DataAnalysis.RecognizeNullTerminatedStrings(mFileData,
rng.Low, rng.High, charTest, scanMode == TextScanMode.LowHighAscii);
if (strCount > 0) {
nullTermStringCount += strCount;
} else {
radioStringNullTerm.IsEnabled = false;
nullTermStringCount = -1;
}
}
// Check for strings prefixed with an 8-bit length.
if (radioStringLen8.IsEnabled) {
int strCount = DataAnalysis.RecognizeLen8Strings(mFileData, rng.Low, rng.High,
charTest, scanMode == TextScanMode.LowHighAscii);
if (strCount > 0) {
len8StringCount += strCount;
} else {
radioStringLen8.IsEnabled = false;
len8StringCount = -1;
}
}
// Check for strings prefixed with a 16-bit length.
if (radioStringLen16.IsEnabled) {
int strCount = DataAnalysis.RecognizeLen16Strings(mFileData, rng.Low, rng.High,
charTest, scanMode == TextScanMode.LowHighAscii);
if (strCount > 0) {
len16StringCount += strCount;
} else {
radioStringLen16.IsEnabled = false;
len16StringCount = -1;
}
}
// Check for DCI strings. All strings within the entire range must have the
// same "polarity", e.g. low ASCII terminated by high ASCII.
if (radioStringDci.IsEnabled) {
int strCount = DataAnalysis.RecognizeDciStrings(mFileData, rng.Low, rng.High,
charTest);
if (strCount > 0) {
dciStringCount += strCount;
} else {
radioStringDci.IsEnabled = false;
dciStringCount = -1;
}
}
}
// Update the dialog with string and character counts, summed across all regions.
string UNSUP_STR = (string)FindResource("str_NotApplicable");
string fmt;
fmt = (string)FindResource("str_StringMixed");
string revfmt = (string)FindResource("str_StringMixedReverse");
if (mixedCharOkCount > 0) {
Debug.Assert(radioStringMixed.IsEnabled);
radioStringMixed.Content = string.Format(fmt,
FormatByteCount(mixedCharOkCount), FormatByteCount(mixedCharNotCount));
radioStringMixedReverse.Content = string.Format(revfmt,
FormatByteCount(mixedCharOkCount), FormatByteCount(mixedCharNotCount));
} else {
Debug.Assert(!radioStringMixed.IsEnabled);
radioStringMixed.Content = string.Format(fmt, UNSUP_STR, UNSUP_STR);
radioStringMixedReverse.Content = string.Format(revfmt, UNSUP_STR, UNSUP_STR);
}
Debug.Assert((nullTermStringCount > 0) ^ (radioStringNullTerm.IsEnabled == false));
fmt = (string)FindResource("str_StringNullTerm");
radioStringNullTerm.Content = FormatStringOption(fmt, nullTermStringCount);
Debug.Assert((len8StringCount > 0) ^ (radioStringLen8.IsEnabled == false));
fmt = (string)FindResource("str_StringLen8");
radioStringLen8.Content = FormatStringOption(fmt, len8StringCount);
Debug.Assert((len16StringCount > 0) ^ (radioStringLen16.IsEnabled == false));
fmt = (string)FindResource("str_StringLen16");
radioStringLen16.Content = FormatStringOption(fmt, len16StringCount);
Debug.Assert((dciStringCount > 0) ^ (radioStringDci.IsEnabled == false));
fmt = (string)FindResource("str_StringDci");
radioStringDci.Content = FormatStringOption(fmt, dciStringCount);
// If this invalidated the selected item, reset to Default.
if ((radioStringMixed.IsChecked == true && !radioStringMixed.IsEnabled) ||
(radioStringMixedReverse.IsChecked == true && !radioStringMixedReverse.IsEnabled) ||
(radioStringNullTerm.IsChecked == true && !radioStringNullTerm.IsEnabled) ||
(radioStringLen8.IsChecked == true && !radioStringLen8.IsEnabled) ||
(radioStringLen8.IsChecked == true && !radioStringLen8.IsEnabled) ||
(radioStringDci.IsChecked == true && !radioStringDci.IsEnabled)) {
Debug.WriteLine("Previous selection invalidated");
radioDefaultFormat.IsChecked = true;
}
}
private string FormatByteCount(int count) {
string fmt;
if (count <= 0) {
return (string)FindResource("str_NotApplicable");
} else if (count == 1) {
fmt = (string)FindResource("str_ByteSingleFmt");
} else {
fmt = (string)FindResource("str_BytePluralFmt");
}
return string.Format(fmt, count);
}
private string FormatStringOption(string fmt, int count) {
if (count <= 0) {
return string.Format(fmt, (string)FindResource("str_NotApplicable"));
} else if (count == 1) {
string fmtSingleString = (string)FindResource("str_StringSingleFmt");
return string.Format(fmt,
string.Format(fmtSingleString, count));
} else {
string fmtPluralString = (string)FindResource("str_StringPluralFmt");
return string.Format(fmt,
string.Format(fmtPluralString, count));
}
}
///
/// Determines whether the data in the buffer can be represented as character values.
/// Using ".DD1 'A'" for 0x41 is obvious, but we also allow ".DD2 'A'" for
/// 0x41 0x00. 16-bit character constants are more likely as intermediate
/// operands, but could be found in data areas.
///
/// Number of bytes per character.
/// Word endian-ness.
/// Character test delegate.
/// True if data in all regions can be represented as a character.
private bool IsCompatibleWithCharSet(int wordWidth, bool isBigEndian,
CharEncoding.InclusionTest charTest) {
IEnumerator iter = mSelection.RangeListIterator;
while (iter.MoveNext()) {
TypedRangeSet.TypedRange rng = iter.Current;
Debug.Assert(((rng.High - rng.Low + 1) / wordWidth) * wordWidth ==
rng.High - rng.Low + 1);
for (int i = rng.Low; i <= rng.High; i += wordWidth) {
int val = RawData.GetWord(mFileData, i, wordWidth, isBigEndian);
if (val != (byte)val || !charTest((byte)val)) {
// bad value, fail
return false;
}
}
}
return true;
}
///
/// Configures the dialog controls based on the provided format descriptor. If
/// the desired options are unavailable, a suitable default is selected instead.
///
/// Call from the Loaded event.
///
/// FormatDescriptor to use.
private void SetControlsFromDescriptor(FormatDescriptor dfd) {
radioSimpleDataHex.IsChecked = true;
radioSymbolPartLow.IsChecked = true;
// Get the previous mode selected in the combo box. If the format descriptor
// doesn't specify a string, we'll use this.
TextScanMode textMode = (TextScanMode)AppSettings.Global.GetEnum(
AppSettings.OPED_DEFAULT_STRING_ENCODING, typeof(TextScanMode),
(int)TextScanMode.LowHighAscii);
if (dfd == null) {
radioDefaultFormat.IsChecked = true;
SetStringEncoding(textMode);
return;
}
if (dfd.IsString) {
textMode = TextScanModeFromDescriptor(dfd);
}
RadioButton preferredFormat;
switch (dfd.FormatType) {
case FormatDescriptor.Type.NumericLE:
case FormatDescriptor.Type.NumericBE:
switch (dfd.Length) {
case 1:
preferredFormat = radioSingleBytes;
break;
case 2:
preferredFormat =
(dfd.FormatType == FormatDescriptor.Type.NumericLE ?
radio16BitLittle : radio16BitBig);
break;
case 3:
preferredFormat = radio24BitLittle;
break;
case 4:
preferredFormat = radio32BitLittle;
break;
default:
Debug.Assert(false);
preferredFormat = radioDefaultFormat;
break;
}
if (preferredFormat.IsEnabled) {
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
case FormatDescriptor.SubType.Hex:
radioSimpleDataHex.IsChecked = true;
break;
case FormatDescriptor.SubType.Decimal:
radioSimpleDataDecimal.IsChecked = true;
break;
case FormatDescriptor.SubType.Binary:
radioSimpleDataBinary.IsChecked = true;
break;
case FormatDescriptor.SubType.Ascii:
case FormatDescriptor.SubType.HighAscii:
radioSimpleDataAscii.IsChecked = true;
break;
case FormatDescriptor.SubType.C64Petscii:
radioSimpleDataPetscii.IsChecked = true;
break;
case FormatDescriptor.SubType.C64Screen:
radioSimpleDataScreenCode.IsChecked = true;
break;
case FormatDescriptor.SubType.Address:
radioSimpleDataAddress.IsChecked = true;
break;
case FormatDescriptor.SubType.Symbol:
radioSimpleDataSymbolic.IsChecked = true;
switch (dfd.SymbolRef.ValuePart) {
case WeakSymbolRef.Part.Low:
radioSymbolPartLow.IsChecked = true;
break;
case WeakSymbolRef.Part.High:
radioSymbolPartHigh.IsChecked = true;
break;
case WeakSymbolRef.Part.Bank:
radioSymbolPartBank.IsChecked = true;
break;
default:
Debug.Assert(false);
break;
}
Debug.Assert(dfd.HasSymbol);
symbolEntryTextBox.Text = Symbol.ConvertLabelForDisplay(
dfd.SymbolRef.Label, Symbol.LabelAnnotation.None,
true, mFormatter);
break;
default:
Debug.Assert(false);
break;
}
} else {
// preferred format not enabled; leave Hex/Low checked
}
break;
case FormatDescriptor.Type.StringGeneric:
preferredFormat = radioStringMixed;
break;
case FormatDescriptor.Type.StringReverse:
preferredFormat = radioStringMixedReverse;
break;
case FormatDescriptor.Type.StringNullTerm:
preferredFormat = radioStringNullTerm;
break;
case FormatDescriptor.Type.StringL8:
preferredFormat = radioStringLen8;
break;
case FormatDescriptor.Type.StringL16:
preferredFormat = radioStringLen16;
break;
case FormatDescriptor.Type.StringDci:
preferredFormat = radioStringDci;
break;
case FormatDescriptor.Type.Dense:
preferredFormat = radioDenseHex;
break;
case FormatDescriptor.Type.Fill:
preferredFormat = radioFill;
break;
case FormatDescriptor.Type.Junk:
preferredFormat = radioJunk;
break;
default:
// Should not be here.
Debug.Assert(false);
preferredFormat = radioDefaultFormat;
break;
}
if (preferredFormat.IsEnabled) {
preferredFormat.IsChecked = true;
} else {
mPreferredFormatUnavailable = true;
radioDefaultFormat.IsChecked = true;
}
SetStringEncoding(textMode);
}
private TextScanMode TextScanModeFromDescriptor(FormatDescriptor dfd) {
Debug.Assert(dfd.IsString);
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.Ascii:
case FormatDescriptor.SubType.HighAscii:
return TextScanMode.LowHighAscii;
case FormatDescriptor.SubType.C64Petscii:
return TextScanMode.C64Petscii;
case FormatDescriptor.SubType.C64Screen:
return TextScanMode.C64ScreenCode;
default:
Debug.Assert(false);
return TextScanMode.LowHighAscii;
}
}
#endregion Setup
#region FormatDescriptor creation
///
/// Creates a list of FormatDescriptors, based on the current control configuration.
///
/// The entries in the list are guaranteed to be sorted by start address and not
/// overlap.
///
/// We assume that whatever the control gives us is correct, e.g. it's not going
/// to tell us to put a buffer full of zeroes into a DCI string.
///
/// Result list.
private void CreateDescriptorListFromControls() {
FormatDescriptor.Type type = FormatDescriptor.Type.Default;
FormatDescriptor.SubType subType = FormatDescriptor.SubType.None;
WeakSymbolRef symbolRef = null;
FormatDescriptor.SubType charSubType;
CharEncoding.InclusionTest charTest;
StringEncodingItem item = (StringEncodingItem)stringEncodingComboBox.SelectedItem;
switch (item.Mode) {
case TextScanMode.LowAscii:
charSubType = FormatDescriptor.SubType.Ascii;
charTest = CharEncoding.IsExtendedAscii;
break;
case TextScanMode.LowHighAscii:
charSubType = FormatDescriptor.SubType.ASCII_GENERIC;
charTest = CharEncoding.IsExtendedLowOrHighAscii;
break;
case TextScanMode.C64Petscii:
charSubType = FormatDescriptor.SubType.C64Petscii;
charTest = CharEncoding.IsExtendedC64Petscii;
break;
case TextScanMode.C64ScreenCode:
charSubType = FormatDescriptor.SubType.C64Screen;
charTest = CharEncoding.IsExtendedC64ScreenCode;
break;
default:
Debug.Assert(false);
charSubType = FormatDescriptor.SubType.ASCII_GENERIC;
charTest = CharEncoding.IsExtendedLowOrHighAscii;
break;
}
// Decode the "display as" panel, if it's relevant.
if (radioSimpleDataHex.IsEnabled) {
if (radioSimpleDataHex.IsChecked == true) {
subType = FormatDescriptor.SubType.Hex;
} else if (radioSimpleDataDecimal.IsChecked == true) {
subType = FormatDescriptor.SubType.Decimal;
} else if (radioSimpleDataBinary.IsChecked == true) {
subType = FormatDescriptor.SubType.Binary;
} else if (radioSimpleDataAscii.IsChecked == true) {
subType = FormatDescriptor.SubType.ASCII_GENERIC;
} else if (radioSimpleDataPetscii.IsChecked == true) {
subType = FormatDescriptor.SubType.C64Petscii;
} else if (radioSimpleDataScreenCode.IsChecked == true) {
subType = FormatDescriptor.SubType.C64Screen;
} else if (radioSimpleDataAddress.IsChecked == true) {
subType = FormatDescriptor.SubType.Address;
} else if (radioSimpleDataSymbolic.IsChecked == true) {
WeakSymbolRef.Part part;
if (radioSymbolPartLow.IsChecked == true) {
part = WeakSymbolRef.Part.Low;
} else if (radioSymbolPartHigh.IsChecked == true) {
part = WeakSymbolRef.Part.High;
} else if (radioSymbolPartBank.IsChecked == true) {
part = WeakSymbolRef.Part.Bank;
} else {
Debug.Assert(false);
part = WeakSymbolRef.Part.Low;
}
subType = FormatDescriptor.SubType.Symbol;
string weakLabel = symbolEntryTextBox.Text;
// Deal with non-unique labels. If the label refers to an existing
// symbol, use its label, which will have the tag. If the label doesn't
// have a match, discard it -- we don't support weak refs to ambiguous
// non-unique symbols.
string trimLabel = Symbol.TrimAndValidateLabel(weakLabel,
mFormatter.NonUniqueLabelPrefix, out bool isValid, out bool unused1,
out bool unused2, out bool hasNonUniquePrefix,
out Symbol.LabelAnnotation unused3);
if (isValid && hasNonUniquePrefix) {
// We want to find the match that's closest to the thing we're
// referencing, but that's awkward when there's multiple ranges and
// multiple ways of interpreting the data. So as a simple measure
// we just grab the lowest offset in the first range.
IEnumerator oiter = mSelection.RangeListIterator;
oiter.MoveNext();
TypedRangeSet.TypedRange rng = oiter.Current;
int matchOffset = rng.Low;
Symbol osym = mProject.FindBestNonUniqueLabel(trimLabel, matchOffset);
if (osym != null) {
trimLabel = osym.Label;
} else {
Debug.WriteLine("Attempt to create ref to nonexistant non-unique sym");
subType = FormatDescriptor.SubType.Hex;
}
}
symbolRef = new WeakSymbolRef(trimLabel, part);
} else {
Debug.Assert(false);
}
} else {
subType = 0; // set later, or doesn't matter
}
// Decode the main format.
int chunkLength = -1;
if (radioDefaultFormat.IsChecked == true) {
// Default/None; note this would create a multi-byte Default format, which isn't
// really allowed. What we actually want to do is remove the explicit formatting
// from all spanned offsets, so we use a dedicated type for that.
type = FormatDescriptor.Type.REMOVE;
} else if (radioSingleBytes.IsChecked == true) {
type = FormatDescriptor.Type.NumericLE;
chunkLength = 1;
} else if (radio16BitLittle.IsChecked == true) {
type = FormatDescriptor.Type.NumericLE;
chunkLength = 2;
} else if (radio16BitBig.IsChecked == true) {
type = FormatDescriptor.Type.NumericBE;
chunkLength = 2;
} else if (radio24BitLittle.IsChecked == true) {
type = FormatDescriptor.Type.NumericLE;
chunkLength = 3;
} else if (radio32BitLittle.IsChecked == true) {
type = FormatDescriptor.Type.NumericLE;
chunkLength = 4;
} else if (radioDenseHex.IsChecked == true || radioDenseHexLimited.IsChecked == true) {
type = FormatDescriptor.Type.Dense;
} else if (radioFill.IsChecked == true) {
type = FormatDescriptor.Type.Fill;
} else if (radioJunk.IsChecked == true) {
type = FormatDescriptor.Type.Junk;
JunkAlignmentItem comboItem = (JunkAlignmentItem)junkAlignComboBox.SelectedItem;
subType = comboItem.FormatSubType;
} else if (radioStringMixed.IsChecked == true) {
type = FormatDescriptor.Type.StringGeneric;
subType = charSubType;
} else if (radioStringMixedReverse.IsChecked == true) {
type = FormatDescriptor.Type.StringReverse;
subType = charSubType;
} else if (radioStringNullTerm.IsChecked == true) {
type = FormatDescriptor.Type.StringNullTerm;
subType = charSubType;
} else if (radioStringLen8.IsChecked == true) {
type = FormatDescriptor.Type.StringL8;
subType = charSubType;
} else if (radioStringLen16.IsChecked == true) {
type = FormatDescriptor.Type.StringL16;
subType = charSubType;
} else if (radioStringDci.IsChecked == true) {
type = FormatDescriptor.Type.StringDci;
subType = charSubType;
} else {
Debug.Assert(false);
// default/none
}
Results = new SortedList();
IEnumerator iter = mSelection.RangeListIterator;
while (iter.MoveNext()) {
TypedRangeSet.TypedRange rng = iter.Current;
switch (type) {
case FormatDescriptor.Type.StringGeneric:
CreateMixedStringEntries(rng.Low, rng.High, type, subType, charTest);
break;
case FormatDescriptor.Type.StringReverse:
CreateMixedStringEntries(rng.Low, rng.High, type, subType, charTest);
break;
case FormatDescriptor.Type.StringNullTerm:
CreateCStringEntries(rng.Low, rng.High, type, subType);
break;
case FormatDescriptor.Type.StringL8:
case FormatDescriptor.Type.StringL16:
CreateLengthStringEntries(rng.Low, rng.High, type, subType);
break;
case FormatDescriptor.Type.StringDci:
CreateDciStringEntries(rng.Low, rng.High, type, subType);
break;
case FormatDescriptor.Type.Dense:
if (radioDenseHexLimited.IsChecked == true) {
int low = rng.Low;
while (low <= rng.High) {
int remaining = rng.High - low + 1;
int subLen = Math.Min(remaining, MaxDenseBytesPerLine);
CreateSimpleEntries(type, subType, -1, null, low, low + subLen - 1);
low += subLen;
chunkLength -= subLen;
}
} else {
CreateSimpleEntries(type, subType, chunkLength, null,
rng.Low, rng.High);
}
break;
default:
CreateSimpleEntries(type, subType, chunkLength, symbolRef,
rng.Low, rng.High);
break;
}
}
}
///
/// Creates one or more FormatDescriptor entries for the specified range, adding them
/// to the Results list.
///
/// This will either create one entry that spans the entire range (for e.g. strings
/// and bulk data), or create equal-sized chunks.
///
/// Region data type.
/// Region data sub-type.
/// Length of a chunk, or -1 for full buffer.
/// Symbol reference, or null if not applicable.
/// Offset of first byte in range.
/// Offset of last byte in range.
private void CreateSimpleEntries(FormatDescriptor.Type type,
FormatDescriptor.SubType subType, int chunkLength,
WeakSymbolRef symbolRef, int low, int high) {
if (chunkLength == -1) {
chunkLength = (high - low) + 1;
}
Debug.Assert(((high - low + 1) / chunkLength) * chunkLength == high - low + 1);
// Either we have one chunk, or we have multiple chunks with the same type and
// length. Either way, we only need to create the descriptor once. (This is
// safe because FormatDescriptor instances are immutable.)
//
// The one exception to this is ASCII values for non-string data, because we have
// to dig the low vs. high value out of the data itself.
FormatDescriptor dfd;
if (subType == FormatDescriptor.SubType.Symbol) {
dfd = FormatDescriptor.Create(chunkLength, symbolRef,
type == FormatDescriptor.Type.NumericBE);
} else {
dfd = FormatDescriptor.Create(chunkLength, type, subType);
}
while (low <= high) {
if (subType == FormatDescriptor.SubType.ASCII_GENERIC) {
// should not be REMOVE with a meaningful subtype
Debug.Assert(dfd.IsNumeric);
int val = RawData.GetWord(mFileData, low, dfd.Length,
type == FormatDescriptor.Type.NumericBE);
FormatDescriptor.SubType actualSubType = (val > 0x7f) ?
FormatDescriptor.SubType.HighAscii : FormatDescriptor.SubType.Ascii;
if (actualSubType != dfd.FormatSubType) {
// replace the descriptor
dfd = FormatDescriptor.Create(chunkLength, type, actualSubType);
}
}
Results.Add(low, dfd);
low += chunkLength;
}
}
///
/// Creates one or more FormatDescriptor entries for the specified range, adding them
/// to the Results list. Runs of character data are output as generic strings, while any
/// non-character data is output as individual bytes.
///
///
/// This is the only string create function that accepts a mix of valid and invalid
/// characters.
///
/// Offset of first byte in range.
/// Offset of last byte in range.
/// String type (Generic or Reverse).
/// String sub-type.
/// Character test delegate.
private void CreateMixedStringEntries(int low, int high, FormatDescriptor.Type type,
FormatDescriptor.SubType subType, CharEncoding.InclusionTest charTest) {
int stringStart = -1;
int cur;
if (subType == FormatDescriptor.SubType.ASCII_GENERIC) {
int highBit = 0;
for (cur = low; cur <= high; cur++) {
byte val = mFileData[cur];
if (charTest(val)) {
// is ASCII
if (stringStart >= 0) {
// was in a string
if (highBit != (val & 0x80)) {
// end of string due to high bit flip, output
CreateGenericStringOrByte(stringStart, cur - stringStart,
type, subType);
// start a new string
stringStart = cur;
} else {
// still in string, keep going
}
} else {
// wasn't in a string, start one
stringStart = cur;
}
highBit = val & 0x80;
} else {
// not ASCII
if (stringStart >= 0) {
// was in a string, output it
CreateGenericStringOrByte(stringStart, cur - stringStart,
type, subType);
stringStart = -1;
}
// output as single byte
CreateByteFD(cur, FormatDescriptor.SubType.Hex);
}
}
} else {
for (cur = low; cur <= high; cur++) {
byte val = mFileData[cur];
if (charTest(val)) {
// is character
if (stringStart < 0) {
// mark this as the start of the string
stringStart = cur;
}
} else {
// not character
if (stringStart >= 0) {
// was in a string, output it
CreateGenericStringOrByte(stringStart, cur - stringStart,
type, subType);
stringStart = -1;
}
// output as single byte
CreateByteFD(cur, FormatDescriptor.SubType.Hex);
}
}
}
if (stringStart >= 0) {
// close out the string
CreateGenericStringOrByte(stringStart, cur - stringStart, type, subType);
}
}
private FormatDescriptor.SubType ResolveAsciiGeneric(int offset,
FormatDescriptor.SubType subType) {
if (subType == FormatDescriptor.SubType.ASCII_GENERIC) {
if ((mFileData[offset] & 0x80) != 0) {
subType = FormatDescriptor.SubType.HighAscii;
} else {
subType = FormatDescriptor.SubType.Ascii;
}
}
return subType;
}
///
/// Creates a format descriptor for character data. If the data is only one byte long,
/// a single-byte character item is emitted instead.
///
/// Offset of first byte.
/// Length of string.
/// String type (Generic or Reverse).
/// String sub-type. If set to ASCII_GENERIC, this will
/// refine the sub-type.
private void CreateGenericStringOrByte(int offset, int length,
FormatDescriptor.Type type, FormatDescriptor.SubType subType) {
Debug.Assert(length > 0);
subType = ResolveAsciiGeneric(offset, subType);
if (length == 1) {
// Single byte, output as single char rather than 1-byte string. We use the
// same encoding as the rest of the string.
CreateByteFD(offset, subType);
} else {
FormatDescriptor dfd;
dfd = FormatDescriptor.Create(length, type, subType);
Results.Add(offset, dfd);
}
}
///
/// Creates a format descriptor for a single-byte numeric value.
///
/// File offset.
/// How to format the item.
private void CreateByteFD(int offset, FormatDescriptor.SubType subType) {
FormatDescriptor dfd = FormatDescriptor.Create(1,
FormatDescriptor.Type.NumericLE, subType);
Results.Add(offset, dfd);
}
///
/// Creates one or more FormatDescriptor entries for the specified range, adding them
/// to the Results list.
///
/// Offset of first byte in range.
/// Offset of last byte in range.
/// String sub-type.
private void CreateCStringEntries(int low, int high, FormatDescriptor.Type type,
FormatDescriptor.SubType subType) {
int startOffset = low;
for (int i = low; i <= high; i++) {
if (mFileData[i] == 0x00) {
// End of string. Zero-length strings are allowed.
FormatDescriptor dfd = FormatDescriptor.Create(
i - startOffset + 1, type, ResolveAsciiGeneric(startOffset, subType));
Results.Add(startOffset, dfd);
startOffset = i + 1;
} else {
// keep going
}
}
// Earlier analysis guaranteed that the last byte in the buffer is 0x00.
Debug.Assert(startOffset == high + 1);
}
///
/// Creates one or more FormatDescriptor entries for the specified range, adding them
/// to the Results list.
///
/// Offset of first byte in range.
/// Offset of last byte in range.
/// String sub-type.
private void CreateLengthStringEntries(int low, int high, FormatDescriptor.Type type,
FormatDescriptor.SubType subType) {
int i;
for (i = low; i <= high;) {
int length = mFileData[i];
if (type == FormatDescriptor.Type.StringL16) {
length |= mFileData[i + 1] << 8;
length += 2;
} else {
length++;
}
// Zero-length strings are allowed.
FormatDescriptor dfd = FormatDescriptor.Create(length, type,
ResolveAsciiGeneric(i, subType));
Results.Add(i, dfd);
i += length;
}
Debug.Assert(i == high + 1);
}
///
/// Creates one or more FormatDescriptor entries for the specified range, adding them
/// to the Results list.
///
/// Offset of first byte in range.
/// Offset of last byte in range.
/// String sub-type.
private void CreateDciStringEntries(int low, int high, FormatDescriptor.Type type,
FormatDescriptor.SubType subType) {
int end, endMask;
end = high + 1;
// Zero-length strings aren't a thing for DCI. The analyzer requires that all
// strings in a region have the same polarity, so just grab the last byte.
endMask = mFileData[end - 1] & 0x80;
int stringStart = low;
for (int i = low; i != end; i++) {
byte val = mFileData[i];
if ((val & 0x80) == endMask) {
// found the end of a string
int length = (i - stringStart) + 1;
FormatDescriptor dfd = FormatDescriptor.Create(length, type,
ResolveAsciiGeneric(stringStart, subType));
Results.Add(stringStart, dfd);
stringStart = i + 1;
}
}
Debug.Assert(stringStart == end);
}
#endregion FormatDescriptor creation
}
}