diff --git a/CommonWPF/CommonWPF.csproj b/CommonWPF/CommonWPF.csproj index 55ae103..f23cf2c 100644 --- a/CommonWPF/CommonWPF.csproj +++ b/CommonWPF/CommonWPF.csproj @@ -54,6 +54,7 @@ + Code diff --git a/CommonWPF/ListToStringConverter.cs b/CommonWPF/ListToStringConverter.cs new file mode 100644 index 0000000..e92d256 --- /dev/null +++ b/CommonWPF/ListToStringConverter.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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.Diagnostics; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace CommonWPF { + /// + /// Converts a List<string> to a multi-line string, suitable for presentation + /// in a TextBlock or other UI element. + /// + /// + /// https://stackoverflow.com/a/345515/294248 + /// + /// In XAML, reference with: + /// xmlns:common="clr-namespace:CommonWPF;assembly=CommonWPF" + /// + [ValueConversion(typeof(List), typeof(string))] + public class ListToStringConverter : IValueConverter { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (targetType != typeof(string)) { + Debug.WriteLine("Invalid targetType for string conversion"); + return DependencyProperty.UnsetValue; + } + + return string.Join("\r\n", ((List)value).ToArray()); + } + + public object ConvertBack(object value, Type targetType, object parameter, + CultureInfo culture) { + return DependencyProperty.UnsetValue; + } + } +} diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index 0fbd915..4cb8fad 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -4177,7 +4177,7 @@ namespace SourceGen { } Tools.Omf.WpfGui.OmfViewer ov = - new Tools.Omf.WpfGui.OmfViewer(this.mMainWin, pathName, fileData); + new Tools.Omf.WpfGui.OmfViewer(this.mMainWin, pathName, fileData, mFormatter); ov.ShowDialog(); } diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index 445532f..a5bbf86 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -87,6 +87,9 @@ + + OmfSegmentViewer.xaml + OmfViewer.xaml @@ -281,6 +284,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/SourceGen/Tools/Omf/OmfFile.cs b/SourceGen/Tools/Omf/OmfFile.cs index 0e158fc..deab4c3 100644 --- a/SourceGen/Tools/Omf/OmfFile.cs +++ b/SourceGen/Tools/Omf/OmfFile.cs @@ -38,8 +38,8 @@ namespace SourceGen.Tools.Omf { /// Appendix F describes OMF v2.1, and Chapter 8 has some useful information about /// how the loader works (e.g. page 205). /// - "Undocumented Secrets of the Apple IIGS System Loader" by Neil Parker, - /// http://nparker.llx.com/a2/loader.html . Among other things it documents ExpressLoad - /// segments, something Apple apparently never did. + /// http://nparker.llx.com/a2/loader.html . Among other things it documents the + /// contents of ExpressLoad segments, which I haven't found in an official reference. /// - Apple IIgs Tech Note #66, "ExpressLoad Philosophy". /// /// Related: @@ -76,19 +76,20 @@ namespace SourceGen.Tools.Omf { Object, // output of assembler/compiler, before linking Library, // static code library RunTimeLibrary, // dynamic shared library + Indeterminate, // valid OMF, but type is indeterminate Foreign // not OMF, or not IIgs OMF } public FileKind OmfFileKind { get; private set; } private bool mIsDamaged; - private string mDamageMsg = string.Empty; - private List mSegmentList = new List(); public List SegmentList { get { return mSegmentList; } } + public List MessageList { get; private set; } = new List(); + /// /// Constructor. @@ -105,7 +106,13 @@ namespace SourceGen.Tools.Omf { OmfSegment.ParseResult result = DoAnalyze(false); if (result == OmfSegment.ParseResult.IsLibrary || result == OmfSegment.ParseResult.Failure) { - DoAnalyze(true); + // Failed; try again as a library. + List firstFail = new List(MessageList); + result = DoAnalyze(true); + if (result == OmfSegment.ParseResult.Failure) { + // Failed both ways. Report the failures from the non-library attempt. + MessageList = firstFail; + } } } @@ -114,24 +121,32 @@ namespace SourceGen.Tools.Omf { int offset = 0; int len = mFileData.Length; + List msgs = new List(); + while (len > 0) { - OmfSegment.ParseResult result = - OmfSegment.ParseSegment(mFileData, offset, parseAsLibrary, out OmfSegment seg); - if (result == OmfSegment.ParseResult.Failure) { - // parsing failed; reject file or stop early - if (first) { - OmfFileKind = FileKind.Foreign; - } else { - mIsDamaged = true; - mDamageMsg = string.Format("File may be damaged; ignoring last {0} bytes", - mFileData.Length - offset); - } - return result; - } else if (result == OmfSegment.ParseResult.IsLibrary) { + OmfSegment.ParseResult result = OmfSegment.ParseSegment(mFileData, offset, + parseAsLibrary, msgs, out OmfSegment seg); + + MessageList.Clear(); + foreach (string str in msgs) { + MessageList.Add(str); + } + + if (result == OmfSegment.ParseResult.IsLibrary) { // Need to start over in library mode. Debug.WriteLine("Restarting in library mode"); return result; + } else if (result == OmfSegment.ParseResult.Failure) { + // Could be a library we failed to parse, could be a totally bad file. + // If we were on the first segment, fail immediately so we can retry as + // library. If not, it's probably not a library (assuming the Library + // Dictionary segment appears first), but rather a partially-bad OMF. + if (first) { + return result; + } + break; } + first = false; Debug.Assert(seg.FileLength > 0); mSegmentList.Add(seg); diff --git a/SourceGen/Tools/Omf/OmfSegment.cs b/SourceGen/Tools/Omf/OmfSegment.cs index 7c24821..147437a 100644 --- a/SourceGen/Tools/Omf/OmfSegment.cs +++ b/SourceGen/Tools/Omf/OmfSegment.cs @@ -76,15 +76,23 @@ namespace SourceGen.Tools.Omf { private const int LOAD_NAME_LEN = 10; public class NameValueNote { - public string Name { get; set; } - public object Value { get; set; } - public string Note { get; set; } + public string Name { get; private set; } + public object Value { get; private set; } + public int Width { get; private set; } + public string Note { get; private set; } + + public NameValueNote(string name, object value, int width, string note) { + Name = name; + Value = value; + Width = width; + Note = note; + } } /// /// Values pulled from file header. Useful for display. /// - List RawValues = new List(); + public List RawValues = new List(); public enum SegmentVersion { v0_0, v1_0, v2_0, v2_1 } @@ -107,9 +115,9 @@ namespace SourceGen.Tools.Omf { BankRel = 0x0100, // v2.1 Skip = 0x0200, // v2.1 Reloadable = 0x0400, // v2.0 - AbsoluteBank = 0x0800, // v2.0 + AbsBank = 0x0800, // v2.0 NoSpecial = 0x1000, // v2.0 - PositionIndep = 0x2000, // + PosnIndep = 0x2000, // Private = 0x4000, // Dynamic = 0x8000 // } @@ -118,6 +126,7 @@ namespace SourceGen.Tools.Omf { // Header fields. // + public int FileOffset { get; private set; } public int FileLength { get; private set; } // from BLKCNT or BYTECNT public int ResSpc { get; private set; } public int Length { get; private set; } @@ -155,19 +164,31 @@ namespace SourceGen.Tools.Omf { IsLibrary } + /// + /// Parses an OMF segment header. + /// + /// File data. + /// Offset at which to start parsing. + /// Set to true to parse the header as if it were part + /// of a library file. Affects parsing of v1 headers. + /// Notes and errors generated by the parser. + /// Completed object, or null on failure. + /// Result code. public static ParseResult ParseSegment(byte[] data, int offset, bool parseAsLibrary, - out OmfSegment segResult) { + List msgs, out OmfSegment segResult) { segResult = null; + //Debug.WriteLine("PARSE offset=" + offset); + Debug.Assert(offset < data.Length); if (data.Length - offset < MIN_HEADER_V0) { // Definitely too small. + AddErrorMsg(msgs, offset, "remaining file space too small to hold segment"); return ParseResult.Failure; } - //Debug.WriteLine("PARSE offset=" + offset); - OmfSegment newSeg = new OmfSegment(); + newSeg.FileOffset = offset; // Start with the version number. The meaning of everything else depends on this. int minLen, expectedDispName; @@ -189,10 +210,13 @@ namespace SourceGen.Tools.Omf { break; default: // invalid version, this is probably not OMF + AddErrorMsg(msgs, offset, "invalid segment type " + data[offset + 0x0f]); return ParseResult.Failure; } if (data.Length - offset < minLen) { // Too small for this version of the header. + AddErrorMsg(msgs, offset, "remaining file space too small to hold " + + newSeg.Version + " segment"); return ParseResult.Failure; } @@ -227,6 +251,8 @@ namespace SourceGen.Tools.Omf { expectedDispName += 4; if (data.Length - offset < minLen + 4) { + AddErrorMsg(msgs, offset, "remaining file space too small to hold " + + newSeg.Version + " segment"); return ParseResult.Failure; } newSeg.TempOrg = RawData.GetWord(data, offset + 0x2c, 4, false); @@ -238,14 +264,14 @@ namespace SourceGen.Tools.Omf { kindByte = data[offset + 0x0c]; if (!Enum.IsDefined(typeof(SegmentKind), kindByte & 0x1f)) { // Example: Moria GS has a kind of $1F for its GLOBALS segment. - Debug.WriteLine("Invalid segment kind $" + kindByte.ToString("x2")); + AddErrorMsg(msgs, offset, "invalid segment kind $" + kindByte.ToString("x2")); return ParseResult.Failure; } newSeg.Kind = (SegmentKind)(kindByte & 0x1f); int kindAttrs = 0; if ((kindByte & 0x20) != 0) { - kindAttrs |= (int)SegmentAttribute.PositionIndep; + kindAttrs |= (int)SegmentAttribute.PosnIndep; } if ((kindByte & 0x40) != 0) { kindAttrs |= (int)SegmentAttribute.Private; @@ -258,7 +284,7 @@ namespace SourceGen.Tools.Omf { // Yank all the attribute bits out at once. Don't worry about v2.0 vs. v2.1. kindWord = RawData.GetWord(data, offset + 0x14, 2, false); if (!Enum.IsDefined(typeof(SegmentKind), kindWord & 0x001f)) { - Debug.WriteLine("Invalid segment kind $" + kindWord.ToString("x4")); + AddErrorMsg(msgs, offset, "invalid segment kind $" + kindWord.ToString("x4")); return ParseResult.Failure; } newSeg.Kind = (SegmentKind)(kindWord & 0x001f); @@ -268,6 +294,7 @@ namespace SourceGen.Tools.Omf { // If we found a library dictionary segment, and we're not currently handling the // file as a library, reject this and try again. if (newSeg.Kind == SegmentKind.LibraryDict && !parseAsLibrary) { + AddInfoMsg(msgs, offset, "found Library Dictionary segment, retrying as library"); return ParseResult.IsLibrary; } @@ -296,47 +323,57 @@ namespace SourceGen.Tools.Omf { } newSeg.FileLength = segLen; + // // Perform validity checks. If any of these fail, we're probably reading something // that isn't OMF (or, if this isn't the first segment, we might have gone off the // rails at some point). - if (numLen != 4 || numSex != 0) { - Debug.WriteLine("Invalid NUMLEN (" + numLen + ") or NUMSEX (" + numSex + ")"); + // + + if (numLen != 4) { + AddErrorMsg(msgs, offset, "NUMLEN must be 4, was " + numLen); + return ParseResult.Failure; + } + if (numSex != 0) { + AddErrorMsg(msgs, offset, "NUMSEX must be 0, was " + numSex); return ParseResult.Failure; } if (offset + segLen > data.Length) { - // Segment is longer than the file. (This can happen easily in a static lib.) - Debug.WriteLine("Segment exceeds EOF: offset=" + offset + " len=" + data.Length + - " segLen=" + segLen); + // Segment is longer than the file. (This can happen easily in a static lib if + // we're not parsing it as such.) + AddErrorMsg(msgs, offset, "segment file length exceeds EOF (segLen=" + segLen + + ", remaining=" + (data.Length - offset) + ")"); return ParseResult.Failure; } if (dispName < expectedDispName || dispName > (segLen - LOAD_NAME_LEN)) { - Debug.WriteLine("Invalid DISPNAME " + dispName + " segLen=" + segLen); + AddErrorMsg(msgs, offset, "invalid DISPNAME " + dispName + " (expected " + + expectedDispName + ", segLen=" + segLen + ")"); return ParseResult.Failure; } if (dispData < expectedDispName + LOAD_NAME_LEN || dispData > (segLen - 1)) { - Debug.WriteLine("Invalid DISPDATA " + dispData + " segLen=" + segLen); + AddErrorMsg(msgs, offset, "invalid DISPDATA " + dispData + " (expected " + + (expectedDispName + LOAD_NAME_LEN) + ", segLen=" + segLen + ")"); return ParseResult.Failure; } if (newSeg.BankSize > 0x00010000) { - Debug.WriteLine("Invalid BANKSIZE $" + newSeg.BankSize.ToString("x")); + AddErrorMsg(msgs, offset, "invalid BANKSIZE $" + newSeg.BankSize.ToString("x")); return ParseResult.Failure; } if (newSeg.Align > 0x00010000) { - Debug.WriteLine("Invalid ALIGN $" + newSeg.Align.ToString("x")); + AddErrorMsg(msgs, offset, "invalid ALIGN $" + newSeg.Align.ToString("x")); return ParseResult.Failure; } if (newSeg.BankSize != 0x00010000 && newSeg.BankSize != 0) { // This is fine, just a little weird. - Debug.WriteLine("Unusual BANKSIZE $" + newSeg.BankSize.ToString("x6")); + AddInfoMsg(msgs, offset, "unusual BANKSIZE $" + newSeg.BankSize.ToString("x6")); } if (newSeg.Align != 0 && newSeg.Align != 0x0100 && newSeg.Align != 0x00010000) { // Unexpected; the loader will round up. - Debug.WriteLine("Unusual ALIGN $" + newSeg.Align.ToString("x6")); + AddInfoMsg(msgs, offset, "unusual ALIGN $" + newSeg.Align.ToString("x6")); } if (newSeg.Entry != 0 && newSeg.Entry >= newSeg.Length) { // This is invalid, but if we got this far we might as well keep going. - Debug.WriteLine("Invalid ENTRY $" + newSeg.Entry.ToString("x6")); + AddInfoMsg(msgs, offset, "invalid ENTRY $" + newSeg.Entry.ToString("x6")); } // Extract LOADNAME. Fixed-width field, padded with spaces. Except for the @@ -354,29 +391,110 @@ namespace SourceGen.Tools.Omf { // string preceded by length byte int segNameLen = data[offset + segNameStart]; if (segNameStart + 1 + segNameLen > segLen) { - Debug.WriteLine("Var-width SEGNAME ran off end of segment (len=" + - segNameLen + ")"); + AddInfoMsg(msgs, offset, "var-width SEGNAME ran off end of segment (len=" + + segNameLen + ", segLen=" + segLen + ")"); return ParseResult.Failure; } segName = Encoding.ASCII.GetString(data, offset + segNameStart + 1, segNameLen); } else { // fixed-width string if (segNameStart + newSeg.LabLen > segLen) { - Debug.WriteLine("Fixed-width SEGNAME ran off end of segment (len=" + - newSeg.LabLen + ")"); + AddInfoMsg(msgs, offset, "fixed-width SEGNAME ran off end of segment (LABLEN=" + + newSeg.LabLen + ", segLen=" + segLen + ")"); return ParseResult.Failure; } segName = ExtractString(data, offset + segNameStart, newSeg.LabLen); } - Debug.WriteLine("LOADNAME='" + loadName + "' SEGNAME='" + segName + "'"); + //AddInfoMsg(msgs, offset, "GOT LOADNAME='" + loadName + "' SEGNAME='" + segName + "'"); + newSeg.LoadName = loadName; newSeg.SegName = segName; + // + // Populate the "raw data" table. We add the fields shown in the specification in + // the order in which they appear. + // + + if (newSeg.Version == SegmentVersion.v0_0 || + (newSeg.Version == SegmentVersion.v1_0 && !parseAsLibrary)) { + newSeg.AddRaw("BLKCNT", blkByteCnt, 4, "blocks"); + } else { + newSeg.AddRaw("BYTECNT", blkByteCnt, 4, "bytes"); + } + newSeg.AddRaw("RESSPC", newSeg.ResSpc, 4, string.Empty); + newSeg.AddRaw("LENGTH", newSeg.Length, 4, string.Empty); + if (newSeg.Version <= SegmentVersion.v1_0) { + string attrStr = AttrsToString(newSeg.Attrs); + if (!string.IsNullOrEmpty(attrStr)) { + attrStr = " -" + attrStr; + } + newSeg.AddRaw("KIND", data[offset+0x0c], 1, + KindToString(newSeg.Kind) + attrStr); + } else { + newSeg.AddRaw("undefined", data[offset + 0x0c], 1, string.Empty); + } + newSeg.AddRaw("LABLEN", newSeg.LabLen, 1, string.Empty); + newSeg.AddRaw("NUMLEN", numLen, 1, "must be 4"); + newSeg.AddRaw("VERSION", data[offset + 0x0f], 1, VersionToString(newSeg.Version)); + newSeg.AddRaw("BANKSIZE", newSeg.BankSize, 4, string.Empty); + if (newSeg.Version >= SegmentVersion.v2_0) { + string attrStr = AttrsToString(newSeg.Attrs); + if (!string.IsNullOrEmpty(attrStr)) { + attrStr = " -" + attrStr; + } + newSeg.AddRaw("KIND", RawData.GetWord(data, offset + 0x14, 2, false), 2, + KindToString(newSeg.Kind) + attrStr); + newSeg.AddRaw("undefined", RawData.GetWord(data, offset + 0x16, 2, false), 2, + string.Empty); + } else { + newSeg.AddRaw("undefined", RawData.GetWord(data, offset + 0x14, 4, false), 4, + string.Empty); + } + newSeg.AddRaw("ORG", newSeg.Org, 4, string.Empty); + newSeg.AddRaw("ALIGN", newSeg.Align, 4, string.Empty); + newSeg.AddRaw("NUMSEX", numSex, 1, "must be 0"); + if (newSeg.Version == SegmentVersion.v1_0) { + newSeg.AddRaw("LCBANK", newSeg.LcBank, 1, string.Empty); + } else { + newSeg.AddRaw("undefined", data[offset + 0x21], 1, string.Empty); + } + if (newSeg.Version >= SegmentVersion.v1_0) { + newSeg.AddRaw("SEGNUM", newSeg.SegNum, 2, string.Empty); + newSeg.AddRaw("ENTRY", newSeg.Entry, 4, string.Empty); + newSeg.AddRaw("DISPNAME", dispName, 2, string.Empty); + newSeg.AddRaw("DISPDATA", dispData, 2, string.Empty); + if (newSeg.Version >= SegmentVersion.v2_1) { + newSeg.AddRaw("tempORG", newSeg.TempOrg, 4, string.Empty); + } + newSeg.AddRaw("LOADNAME", loadName, 10, string.Empty); + } + newSeg.AddRaw("SEGNAME", segName, 0, string.Empty); + segResult = newSeg; return ParseResult.Success; } + // + // Helper functions. + // + + private void AddRaw(string name, object value, int width, string note) { + if (value is byte) { + value = (int)(byte)value; + } + RawValues.Add(new NameValueNote(name, value, width, note)); + } + private static void AddInfoMsg(List msgs, int offset, string msg) { + msgs.Add("Note (+" + offset.ToString("x6") + "): " + msg); + } + private static void AddErrorMsg(List msgs, int offset, string msg) { + msgs.Add("Error (+" + offset.ToString("x6") + "): " + msg); + } + + /// + /// Extracts a fixed-length ASCII string, stopping early if a '\0' is encountered. + /// private static string ExtractString(byte[] data, int offset, int len) { StringBuilder sb = new StringBuilder(); for (int i = offset; i < offset + len; i++) { @@ -388,5 +506,54 @@ namespace SourceGen.Tools.Omf { } return sb.ToString(); } + + /// + /// Converts a segment version to a human-readable string. + /// + public static string VersionToString(SegmentVersion vers) { + switch (vers) { + case SegmentVersion.v0_0: return "v0.0"; + case SegmentVersion.v1_0: return "v1.0"; + case SegmentVersion.v2_0: return "v2.0"; + case SegmentVersion.v2_1: return "v2.1"; + default: return "v?.?"; + } + } + + /// + /// Converts a segment kind to a human-readable string. + /// + public static string KindToString(SegmentKind kind) { + switch (kind) { + case SegmentKind.Code: return "Code"; + case SegmentKind.Data: return "Data"; + case SegmentKind.JumpTable: return "Jump Table"; + case SegmentKind.PathName: return "Pathname"; + case SegmentKind.LibraryDict: return "Library Dict"; + case SegmentKind.Init: return "Init"; + case SegmentKind.AbsoluteBank: return "Abs Bank"; + case SegmentKind.DpStack: return "DP/Stack"; + default: return "???"; + } + } + + public static string AttrsToString(SegmentAttribute attrs) { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < 16; i++) { + int bit = 1 << i; + if (((int)attrs & bit) != 0) { + SegmentAttribute attr = (SegmentAttribute)bit; + sb.Append(' '); + sb.Append(attr.ToString()); + } + } + return sb.ToString(); + } + + + public override string ToString() { + return "[OmfSegment " + SegNum + " '" + LoadName + "' '" + SegName + "']"; + } } } diff --git a/SourceGen/Tools/Omf/WpfGui/OmfSegmentViewer.xaml b/SourceGen/Tools/Omf/WpfGui/OmfSegmentViewer.xaml new file mode 100644 index 0000000..c9c59f0 --- /dev/null +++ b/SourceGen/Tools/Omf/WpfGui/OmfSegmentViewer.xaml @@ -0,0 +1,135 @@ + + + + + + + + File offset {0}, length {1} ({2}) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +