diff --git a/PluginCommon/Interfaces.cs b/PluginCommon/Interfaces.cs index 9fa19e6..e555255 100644 --- a/PluginCommon/Interfaces.cs +++ b/PluginCommon/Interfaces.cs @@ -454,7 +454,8 @@ namespace PluginCommon { Dense, Fill, Uninit, - Junk + Junk, + BinaryInclude } /// diff --git a/SourceGen/AsmGen/AsmAcme.cs b/SourceGen/AsmGen/AsmAcme.cs index a0e0c57..7ac2fb1 100644 --- a/SourceGen/AsmGen/AsmAcme.cs +++ b/SourceGen/AsmGen/AsmAcme.cs @@ -56,6 +56,11 @@ namespace SourceGen.AsmGen { // IGenerator public int StartOffset { get { return 0; } } + /// + /// List of binary include sections found in the project. + /// + private List mBinaryIncludes = new List(); + /// /// Working directory, i.e. where we write our output file(s). /// @@ -139,6 +144,7 @@ namespace SourceGen.AsmGen { { "Uninit", "!skip" }, //Junk { "Align", "!align" }, + { "BinaryInclude", "!binary" }, { "StrGeneric", "!text" }, // can use !xor for high ASCII //StrReverse //StrNullTerm @@ -303,7 +309,7 @@ namespace SourceGen.AsmGen { } mOutStream = null; - return new GenerationResults(pathNames, string.Empty); + return new GenerationResults(pathNames, string.Empty, mBinaryIncludes); } /// @@ -483,6 +489,12 @@ namespace SourceGen.AsmGen { OutputDenseHex(offset, length, labelStr, commentStr); } break; + case FormatDescriptor.Type.BinaryInclude: + opcodeStr = sDataOpNames.BinaryInclude; + string biPath = BinaryInclude.ConvertPathNameFromStorage(dfd.Extra); + operandStr = '"' + biPath + '"'; + mBinaryIncludes.Add(new BinaryInclude.Excision(offset, length, biPath)); + break; case FormatDescriptor.Type.StringGeneric: case FormatDescriptor.Type.StringReverse: case FormatDescriptor.Type.StringNullTerm: diff --git a/SourceGen/AsmGen/AsmCc65.cs b/SourceGen/AsmGen/AsmCc65.cs index 3baea0c..1b6e309 100644 --- a/SourceGen/AsmGen/AsmCc65.cs +++ b/SourceGen/AsmGen/AsmCc65.cs @@ -51,6 +51,11 @@ namespace SourceGen.AsmGen { // IGenerator public int StartOffset { get { return 0; } } + /// + /// List of binary include sections found in the project. + /// + private List mBinaryIncludes = new List(); + /// /// Working directory, i.e. where we write our output file(s). /// @@ -138,6 +143,8 @@ namespace SourceGen.AsmGen { { "Dense", ".byte" }, // really just just comma-separated bytes { "Uninit", ".res" }, //Junk + //Align + { "BinaryInclude", ".incbin" }, { "StrGeneric", ".byte" }, //StrReverse { "StrNullTerm", ".asciiz" }, @@ -262,7 +269,7 @@ namespace SourceGen.AsmGen { } mOutStream = null; - return new GenerationResults(pathNames, string.Empty); + return new GenerationResults(pathNames, string.Empty, mBinaryIncludes); } private void GenerateLinkerScript(StreamWriter sw) { @@ -470,6 +477,12 @@ namespace SourceGen.AsmGen { OutputDenseHex(offset, length, labelStr, commentStr); } break; + case FormatDescriptor.Type.BinaryInclude: + opcodeStr = sDataOpNames.BinaryInclude; + string biPath = BinaryInclude.ConvertPathNameFromStorage(dfd.Extra); + operandStr = '"' + biPath + '"'; + mBinaryIncludes.Add(new BinaryInclude.Excision(offset, length, biPath)); + break; case FormatDescriptor.Type.StringGeneric: case FormatDescriptor.Type.StringReverse: case FormatDescriptor.Type.StringNullTerm: diff --git a/SourceGen/AsmGen/AsmMerlin32.cs b/SourceGen/AsmGen/AsmMerlin32.cs index 500d910..ad539d2 100644 --- a/SourceGen/AsmGen/AsmMerlin32.cs +++ b/SourceGen/AsmGen/AsmMerlin32.cs @@ -133,6 +133,7 @@ namespace SourceGen.AsmGen { { "Uninit", "ds" }, //Junk //Align + //BinaryInclude { "StrGeneric", "asc" }, { "StrReverse", "rev" }, //StrNullTerm @@ -243,7 +244,8 @@ namespace SourceGen.AsmGen { } mOutStream = null; - return new GenerationResults(pathNames, string.Empty); + return new GenerationResults(pathNames, string.Empty, + new List()); } // IGenerator @@ -316,6 +318,7 @@ namespace SourceGen.AsmGen { break; case FormatDescriptor.Type.Uninit: case FormatDescriptor.Type.Junk: + case FormatDescriptor.Type.BinaryInclude: // not supported, gen minimal output int fillVal = Helper.CheckRangeHoldsSingleValue(data, offset, length); if (fillVal >= 0) { opcodeStr = sDataOpNames.Fill; diff --git a/SourceGen/AsmGen/AsmTass64.cs b/SourceGen/AsmGen/AsmTass64.cs index 658692f..2dffa52 100644 --- a/SourceGen/AsmGen/AsmTass64.cs +++ b/SourceGen/AsmGen/AsmTass64.cs @@ -61,12 +61,18 @@ namespace SourceGen.AsmGen { // IGenerator public LabelLocalizer Localizer { get { return mLocalizer; } } + // IGenerator public int StartOffset { get { return mHasPrgHeader ? 2 : 0; } } + /// + /// List of binary include sections found in the project. + /// + private List mBinaryIncludes = new List(); + /// /// Working directory, i.e. where we write our output file(s). /// @@ -158,6 +164,7 @@ namespace SourceGen.AsmGen { { "Uninit", ".fill" }, //Junk { "Align", ".align" }, + { "BinaryInclude", ".binary" }, { "StrGeneric", ".text" }, //StrReverse { "StrNullTerm", ".null" }, @@ -323,7 +330,7 @@ namespace SourceGen.AsmGen { } mOutStream = null; - return new GenerationResults(pathNames, extraOptions); + return new GenerationResults(pathNames, extraOptions, mBinaryIncludes); } // IGenerator @@ -577,6 +584,12 @@ namespace SourceGen.AsmGen { OutputDenseHex(offset, length, labelStr, commentStr); } break; + case FormatDescriptor.Type.BinaryInclude: + opcodeStr = sDataOpNames.BinaryInclude; + string biPath = BinaryInclude.ConvertPathNameFromStorage(dfd.Extra); + operandStr = '"' + biPath + '"'; + mBinaryIncludes.Add(new BinaryInclude.Excision(offset, length, biPath)); + break; case FormatDescriptor.Type.StringGeneric: case FormatDescriptor.Type.StringReverse: case FormatDescriptor.Type.StringNullTerm: diff --git a/SourceGen/AsmGen/BinaryInclude.cs b/SourceGen/AsmGen/BinaryInclude.cs new file mode 100644 index 0000000..0b413c7 --- /dev/null +++ b/SourceGen/AsmGen/BinaryInclude.cs @@ -0,0 +1,225 @@ +/* + * Copyright 2024 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.IO; + +namespace SourceGen.AsmGen { + /// + /// Helper functions for working with binary includes. + /// + public static class BinaryInclude { + // Character placed at the start of a path as a check that the field holds what we + // expect. If we want to modify the structure of the string, e.g. to add or remove + // additional fields, we can change the character to something else. + private static char PATH_PREFIX_CHAR = '\u2191'; // UPWARDS ARROW + + /// + /// Class to help when gathering up binary includes during asm gen. + /// + public class Excision { + // Offset of start of region to excise. + public int Offset { get; private set; } + + // Length of region to excise. + public int Length { get; private set; } + + // Partial pathname of output file, as stored in the project. + public string PathName { get; private set; } + + // Full output file path, initially null, set by PrepareList(). + public string FullPath { get; set; } + + public Excision(int offset, int length, string pathName) { + Offset = offset; + Length = length; + PathName = pathName; + } + + public override string ToString() { + return "[Exc: offset=+" + Offset.ToString("x6") + " len=" + Length + + "path=\"" + PathName + "\" fullPath=\"" + FullPath + "\"]"; + } + } + + /// + /// Determines the full path of each binary include output file. Checks for duplicates. + /// Sorts the list by case-insensitive pathname. + /// + /// List of binary include excisions. + /// Working directory. + /// On failure, a human-readable error message. + /// True on success. + public static bool PrepareList(List list, string workDir, out string failMsg) { + // Normalize the pathname. This is not expected to fail. + string fullWorkDir = Path.GetFullPath(workDir); + + string oldCurrentDir = Environment.CurrentDirectory; + try { + Environment.CurrentDirectory = workDir; + + foreach (Excision exc in list) { + try { + exc.FullPath = Path.GetFullPath(exc.PathName); + } catch (Exception ex) { + failMsg = "unable to get full path for binary include \"" + + exc.PathName + "\": " + ex.Message; + return false; + } + + if (!exc.FullPath.StartsWith(fullWorkDir)) { + failMsg = "binary include path for \"" + exc.PathName + + "\" resolved to parent directory"; + return false; + } + } + } finally { + Environment.CurrentDirectory = oldCurrentDir; + } + + // Check for duplicates. Assume filenames are case-insensitive. + list.Sort(delegate (Excision a, Excision b) { + return string.Compare(a.PathName, b.PathName, + StringComparison.InvariantCultureIgnoreCase); + }); + string prev = null; + foreach (Excision exc in list) { + if (prev != null && exc.FullPath == prev) { + failMsg = "found multiple binary includes that output to \"" + prev + "\""; + return false; + } + prev = exc.FullPath; + } + + failMsg = string.Empty; + return true; + } + + /// + /// Generates the output file with the binary include data. + /// + /// Binary include object, with full pathname computed. + /// Project data array. + /// On failure, a human-readable error message. + /// True on success. + public static bool GenerateOutputFile(Excision exc, byte[] data, out string failMsg) { + if (exc.FullPath == null) { + failMsg = "internal error"; + return false; + } + if (File.Exists(exc.FullPath)) { + // Test the file length. If it's different, don't overwrite the existing file. + // Make an exception if it's zero bytes long? + long fileLen = new FileInfo(exc.FullPath).Length; + if (exc.Length != fileLen) { + failMsg = "output file \"" + exc.PathName + "\" exists and " + + "has a different length (" + fileLen + " vs. " + exc.Length + ")"; + return false; + } + } + try { + // Create any directories in the path. + string dirName = Path.GetDirectoryName(exc.FullPath); + Directory.CreateDirectory(dirName); + // Create the file and copy the data into it. + Debug.Assert(exc.Offset < data.Length && exc.Offset + exc.Length <= data.Length); + using (Stream stream = new FileStream(exc.FullPath, FileMode.OpenOrCreate, + FileAccess.ReadWrite, FileShare.None)) { + stream.SetLength(0); + stream.Write(data, exc.Offset, exc.Length); + } + } catch (Exception ex) { + failMsg = "unable to create '" + exc.PathName + "': " + ex.Message; + return false; + } + failMsg = string.Empty; + return true; + } + + /// + /// Validates a binary-include filename. We allow partial paths, but they're not allowed + /// to ascend above the current directory. Does not access the filesystem. + /// + /// + /// The Path.GetFullPath() call hits the filesystem, which is undesirable for + /// a check-as-you-type test. We just want to avoid having a "rooted" path or something + /// with a ".." directory reference. + /// This is intended as a simple measure to avoid having important files + /// overwritten by an asm generation command. The file generator could employ other + /// measures, e.g. checking to see if an existing output file has the same size. (Note + /// some malicious individual could hand-edit the filename in the project file.) + /// We screen the filename for illegal characters, though what works on one + /// platform might not on another. We can't guarantee validity. + /// + /// Partial path to verify. + /// True if the path looks correct. + public static bool ValidatePathName(string pathName) { + if (string.IsNullOrEmpty(pathName)) { + return false; + } + // In .NET Framework, IsPathRooted() will throw if invalid chars are found. This is + // not a full syntax check, just a char test. The behavior changed in .NET Core 2.1. + try { + if (Path.IsPathRooted(pathName)) { + return false; + } + } catch (Exception ex) { + Debug.WriteLine("GetFileName rejected pathname: " + ex.Message); + return false; + } + + // Try to screen out "../foo", "x/../y", "bar/..", without rejecting "..my..stuff..". + // Normalize to forward-slash and split into components. + string normal = pathName.Replace('\\', '/'); + string[] parts = normal.Split('/'); + foreach (string part in parts) { + if ("..".Equals(part)) { + return false; + } + } + + // Reject names with a double quote, so we don't have to figure out the quote-quoting + // mechanism for every assembler. + if (normal.Contains("\"")) { + return false; + } + + return true; + } + + /// + /// Converts a binary include pathname to a format suited for storage. + /// + /// Partial pathname. + /// String to store. + public static string ConvertPathNameToStorage(string pathName) { + return PATH_PREFIX_CHAR + pathName; + } + + /// + /// Converts the stored name back to a path prefix string. + /// + /// Stored string. + /// Path prefix. + public static string ConvertPathNameFromStorage(string storageStr) { + if (string.IsNullOrEmpty(storageStr) || storageStr[0] != PATH_PREFIX_CHAR) { + return "!BAD STORED NAME!"; + } + return storageStr.Substring(1); + } + } +} diff --git a/SourceGen/AsmGen/IGenerator.cs b/SourceGen/AsmGen/IGenerator.cs index 62bc727..d838c1d 100644 --- a/SourceGen/AsmGen/IGenerator.cs +++ b/SourceGen/AsmGen/IGenerator.cs @@ -288,10 +288,13 @@ namespace SourceGen.AsmGen { public class GenerationResults { public List PathNames { get; private set; } public string ExtraOptions { get; private set; } + public List BinaryIncludes { get; private set; } - public GenerationResults(List pathNames, string extraOptions) { + public GenerationResults(List pathNames, string extraOptions, + List binaryIncludes) { PathNames = CommonUtil.Container.CopyStringList(pathNames); ExtraOptions = extraOptions; + BinaryIncludes = binaryIncludes; } } -} \ No newline at end of file +} diff --git a/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml.cs b/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml.cs index 2f27509..ad8767f 100644 --- a/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml.cs +++ b/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml.cs @@ -285,6 +285,24 @@ namespace SourceGen.AsmGen.WpfGui { return; } + // Generate binary includes. + if (!BinaryInclude.PrepareList(res.BinaryIncludes, mWorkDirectory, + out string failMsg)) { + MessageBox.Show(this, "Failed processing binary includes: " + failMsg, + Res.Strings.ERR_FILE_GENERIC_CAPTION, + MessageBoxButton.OK, MessageBoxImage.Error); + } else { + foreach (BinaryInclude.Excision exc in res.BinaryIncludes) { + if (!BinaryInclude.GenerateOutputFile(exc, mProject.FileData, + out string failMsg2)) { + MessageBox.Show(this, "Failed processing binary include at +" + + exc.Offset.ToString("x6") + ": " + failMsg2, + Res.Strings.ERR_FILE_GENERIC_CAPTION, + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + ResetElements(); mGenerationResults = res; previewFileComboBox.Items.Clear(); diff --git a/SourceGen/CodeAnalysis.cs b/SourceGen/CodeAnalysis.cs index bf0a7da..99c29a3 100644 --- a/SourceGen/CodeAnalysis.cs +++ b/SourceGen/CodeAnalysis.cs @@ -1189,6 +1189,9 @@ namespace SourceGen { if (type == DataType.Fill && subType != DataSubType.None) { throw new PluginException("SIDF rej: fill data must use subType=None"); } + if (type == DataType.BinaryInclude && subType != DataSubType.None) { + throw new PluginException("SIDF rej: binary-include data must use subType=None"); + } if (isStringType && !isStringSub) { throw new PluginException("SIDF rej: bad type/subType combo: type=" + @@ -1275,6 +1278,8 @@ namespace SourceGen { return FormatDescriptor.Type.Uninit; case DataType.Dense: return FormatDescriptor.Type.Dense; + case DataType.BinaryInclude: + return FormatDescriptor.Type.BinaryInclude; default: Debug.Assert(false); throw new PluginException("Instr format rej: unknown format type " + pluginType); diff --git a/SourceGen/DisasmProject.cs b/SourceGen/DisasmProject.cs index 9bc3f66..d092381 100644 --- a/SourceGen/DisasmProject.cs +++ b/SourceGen/DisasmProject.cs @@ -152,7 +152,7 @@ namespace SourceGen { /// - /// The contents of the 65xx data file. + /// The contents of the 65xx data file. Do not modify. /// public byte[] FileData { get { return mFileData; } } private byte[] mFileData; diff --git a/SourceGen/FormatDescriptor.cs b/SourceGen/FormatDescriptor.cs index fb0ea2d..88dfff3 100644 --- a/SourceGen/FormatDescriptor.cs +++ b/SourceGen/FormatDescriptor.cs @@ -62,7 +62,8 @@ namespace SourceGen { Dense, // raw data, represented as compactly as possible Fill, // fill memory with a value Uninit, // uninitialized data storage area - Junk // contents of memory are not interesting + Junk, // contents of memory are not interesting + BinaryInclude // file contents will be loaded from external file during asm } /// @@ -171,6 +172,19 @@ namespace SourceGen { /// public WeakSymbolRef SymbolRef { get; private set; } + /// + /// Optional extra data, used for special cases like BinaryInclude. May be null. + /// + /// + /// It's unfortunate that we have this field for every object, even though very few + /// will actually make use of it. The SymbolRef field has a very specific purpose + /// and shouldn't be used to hold it (asserts and other logic gets upset). Storing + /// the filenames in a separate table has some advantages, but requires integrating + /// changes with the undo/redo mechanism, and the space savings doesn't justify the + /// complexity cost. + /// + public string Extra { get; private set; } + // Crude attempt to see how effective the prefab object creation is. Note we create // these for DefSymbols, so there will be one prefab for every platform symbol entry. public static int DebugCreateCount { get; private set; } @@ -191,6 +205,7 @@ namespace SourceGen { Debug.Assert(length > 0); Debug.Assert(length <= MAX_NUMERIC_LEN || !IsNumeric); Debug.Assert(fmt != Type.Default || length == 1); + Debug.Assert(fmt != Type.BinaryInclude); Debug.Assert(subFmt == SubType.None || (fmt != Type.Junk) ^ IsJunkSubType(subFmt)); Length = length; @@ -213,6 +228,22 @@ namespace SourceGen { SymbolRef = sym; } + /// + /// Constructor for item with arbitrary string data. + /// + /// Length, in bytes. + /// Format type. + /// String data. + private FormatDescriptor(int length, Type fmt, string stringData) { + Debug.Assert(length > 0); + Debug.Assert(fmt == Type.BinaryInclude); + Debug.Assert(!string.IsNullOrEmpty(stringData)); + Length = length; + FormatType = fmt; + FormatSubType = SubType.None; + Extra = stringData; + } + /// /// Returns a descriptor with the requested characteristics. For common cases this /// returns a pre-allocated object, for less-common cases this allocates a new object. @@ -267,6 +298,18 @@ namespace SourceGen { return new FormatDescriptor(length, sym, isBigEndian); } + /// + /// Returns a descriptor with arbitrary string data. + /// + /// Length, in bytes. + /// Format type. + /// String data. + /// New or pre-allocated descriptor. + public static FormatDescriptor Create(int length, Type fmt, string str) { + DebugCreateCount++; + return new FormatDescriptor(length, fmt, str); + } + /// /// True if the descriptor is okay to use on an instruction operand. The CPU only /// understands little-endian numeric values, so that's all we allow. @@ -276,8 +319,8 @@ namespace SourceGen { switch (FormatType) { case Type.Default: case Type.NumericLE: - //case Type.NumericBE: return true; + //case Type.NumericBE: default: return false; } @@ -506,6 +549,9 @@ namespace SourceGen { case Type.Junk: retstr += "unaligned junk"; break; + case Type.BinaryInclude: + retstr += "binary include"; + break; default: // strings handled earlier retstr += "???"; @@ -571,7 +617,7 @@ namespace SourceGen { public override string ToString() { return "[FmtDesc: len=" + Length + " fmt=" + FormatType + " sub=" + FormatSubType + - " sym=" + SymbolRef + "]"; + " sym=" + SymbolRef + " xtra=" + Extra + "]"; } @@ -583,7 +629,8 @@ namespace SourceGen { return false; // one is null } return a.Length == b.Length && a.FormatType == b.FormatType && - a.FormatSubType == b.FormatSubType && a.SymbolRef == b.SymbolRef; + a.FormatSubType == b.FormatSubType && a.SymbolRef == b.SymbolRef && + a.Extra == b.Extra; } public static bool operator !=(FormatDescriptor a, FormatDescriptor b) { return !(a == b); @@ -599,6 +646,9 @@ namespace SourceGen { hashCode ^= Length; hashCode ^= (int)FormatType; hashCode ^= (int)FormatSubType; + if (Extra != null) { + hashCode ^= Extra.GetHashCode(); + } return hashCode; } diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index fa056b3..1439c00 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -2352,6 +2352,7 @@ namespace SourceGen { Debug.WriteLine("No change to data formats"); } } + } public void EditProjectProperties(WpfGui.EditProjectProperties.Tab initialTab) { diff --git a/SourceGen/ProjectFile.cs b/SourceGen/ProjectFile.cs index 2830ab7..67dbfb1 100644 --- a/SourceGen/ProjectFile.cs +++ b/SourceGen/ProjectFile.cs @@ -23,6 +23,10 @@ using System.Web.Script.Serialization; using CommonUtil; +// TODO: experiment with serialization options that exclude default values, such as null +// strings, from the serialized output +// TODO: switch to System.Text.Json.JsonSerializer (with WriteIndented=true). + namespace SourceGen { /// /// Load and save project data from/to a ".dis65" file. @@ -312,6 +316,7 @@ namespace SourceGen { public string Format { get; set; } public string SubFormat { get; set; } public SerWeakSymbolRef SymbolRef { get; set; } + public string Extra { get; set; } public SerFormatDescriptor() { } public SerFormatDescriptor(FormatDescriptor dfd) { @@ -321,6 +326,9 @@ namespace SourceGen { if (dfd.SymbolRef != null) { SymbolRef = new SerWeakSymbolRef(dfd.SymbolRef); } + if (dfd.Extra != null) { + Extra = dfd.Extra; + } } } public class SerWeakSymbolRef { @@ -1087,9 +1095,14 @@ namespace SourceGen { ": " + sfd.Format + "/" + sfd.SubFormat); return false; } - if (sfd.SymbolRef == null) { + if (sfd.Extra != null) { + // Descriptor with extra data. + dfd = FormatDescriptor.Create(sfd.Length, format, sfd.Extra); + } else if (sfd.SymbolRef == null) { + // Simple descriptor. dfd = FormatDescriptor.Create(sfd.Length, format, subFormat); } else { + // Descriptor with symbolic reference. WeakSymbolRef.Part part; try { part = (WeakSymbolRef.Part)Enum.Parse( diff --git a/SourceGen/PseudoOp.cs b/SourceGen/PseudoOp.cs index cfb4572..cbf8ce4 100644 --- a/SourceGen/PseudoOp.cs +++ b/SourceGen/PseudoOp.cs @@ -81,6 +81,7 @@ namespace SourceGen { public string Uninit { get; private set; } public string Junk { get; private set; } public string Align { get; private set; } + public string BinaryInclude { get; private set; } public string StrGeneric { get; private set; } public string StrReverse { get; private set; } public string StrLen8 { get; private set; } @@ -133,6 +134,7 @@ namespace SourceGen { a.Uninit == b.Uninit && a.Junk == b.Junk && a.Align == b.Align && + a.BinaryInclude == b.BinaryInclude && a.StrGeneric == b.StrGeneric && a.StrReverse == b.StrReverse && a.StrLen8 == b.StrLen8 && @@ -247,6 +249,7 @@ namespace SourceGen { { "Uninit", ".ds" }, { "Junk", ".junk" }, { "Align", ".align" }, + { "BinaryInclude", ".incbin" }, { "StrGeneric", ".str" }, { "StrReverse", ".rstr" }, @@ -280,6 +283,7 @@ namespace SourceGen { case FormatDescriptor.Type.Fill: case FormatDescriptor.Type.Uninit: case FormatDescriptor.Type.Junk: + case FormatDescriptor.Type.BinaryInclude: return 1; case FormatDescriptor.Type.Dense: { // no delimiter, two output bytes per input byte @@ -389,6 +393,11 @@ namespace SourceGen { //po = outList[subIndex]; } break; + case FormatDescriptor.Type.BinaryInclude: + po.Opcode = opNames.BinaryInclude; + string biPath = AsmGen.BinaryInclude.ConvertPathNameFromStorage(dfd.Extra); + po.Operand = '"' + biPath + "'"; + break; default: Debug.Assert(false); po.Opcode = ".???"; diff --git a/SourceGen/SGTestData/20300-binary-include b/SourceGen/SGTestData/20300-binary-include new file mode 100644 index 0000000..a8385eb Binary files /dev/null and b/SourceGen/SGTestData/20300-binary-include differ diff --git a/SourceGen/SGTestData/20300-binary-include.dis65 b/SourceGen/SGTestData/20300-binary-include.dis65 new file mode 100644 index 0000000..34b8b69 --- /dev/null +++ b/SourceGen/SGTestData/20300-binary-include.dis65 @@ -0,0 +1,85 @@ +### 6502bench SourceGen dis65 v1.0 ### +{ +"_ContentVersion":6, +"FileDataLength":96, +"FileDataCrc32":1316780246, +"ProjectProps":{ +"CpuName":"6502", +"IncludeUndocumentedInstr":false, +"TwoByteBrk":false, +"EntryFlags":32702671, +"AutoLabelStyle":"Simple", +"AnalysisParams":{ +"AnalyzeUncategorizedData":true, +"DefaultTextScanMode":"LowHighAscii", +"MinCharsForString":4, +"SeekNearbyTargets":true, +"UseRelocData":false, +"SmartPlpHandling":false, +"SmartPlbHandling":true}, + +"PlatformSymbolFileIdentifiers":[], +"ExtensionScriptFileIdentifiers":[], +"ProjectSyms":{ +}}, + +"AddressMap":[{ +"Offset":0, +"Addr":4096, +"Length":96, +"PreLabel":"", +"DisallowInward":false, +"DisallowOutward":false, +"IsRelative":false}], +"TypeHints":[{ +"Low":0, +"High":0, +"Hint":"Code"}], +"StatusFlagOverrides":{ +}, + +"Comments":{ +}, + +"LongComments":{ +}, + +"Notes":{ +}, + +"UserLabels":{ +"95":{ +"Label":"done", +"Value":4191, +"Source":"User", +"Type":"GlobalAddr", +"LabelAnno":"None"}}, + +"OperandFormats":{ +"23":{ +"Length":40, +"Format":"BinaryInclude", +"SubFormat":"None", +"SymbolRef":null, +"Extra":"↑20300-1.bin"}, + +"63":{ +"Length":32, +"Format":"BinaryInclude", +"SubFormat":"None", +"SymbolRef":null, +"Extra":"↑20300sub/20300-2.bin"}}, + +"LvTables":{ +}, + +"Visualizations":[], +"VisualizationAnimations":[], +"VisualizationSets":{ +}, + +"RelocList":{ +}, + +"DbrValues":{ +}} diff --git a/SourceGen/SGTestData/Expected/20300-binary-include_64tass.S b/SourceGen/SGTestData/Expected/20300-binary-include_64tass.S new file mode 100644 index 0000000..450d67d --- /dev/null +++ b/SourceGen/SGTestData/Expected/20300-binary-include_64tass.S @@ -0,0 +1,17 @@ + .cpu "6502" +* = $1000 +L1000 ldy #$28 + lda _L1017,y + sta $2000,y + dey + bpl L1000 + lda _L103F + lda _L103F+1 + lda done-1 + jmp done + +_L1017 .binary "20300-1.bin" +_L103F .binary "20300sub/20300-2.bin" + +done rts + diff --git a/SourceGen/SGTestData/Expected/20300-binary-include_acme.S b/SourceGen/SGTestData/Expected/20300-binary-include_acme.S new file mode 100644 index 0000000..5b5fab7 --- /dev/null +++ b/SourceGen/SGTestData/Expected/20300-binary-include_acme.S @@ -0,0 +1,17 @@ + !cpu 6502 +* = $1000 +L1000 ldy #$28 + lda @L1017,y + sta $2000,y + dey + bpl L1000 + lda @L103F + lda @L103F+1 + lda done-1 + jmp done + +@L1017 !binary "20300-1.bin" +@L103F !binary "20300sub/20300-2.bin" + +done rts + diff --git a/SourceGen/SGTestData/Expected/20300-binary-include_cc65.S b/SourceGen/SGTestData/Expected/20300-binary-include_cc65.S new file mode 100644 index 0000000..b61e20a --- /dev/null +++ b/SourceGen/SGTestData/Expected/20300-binary-include_cc65.S @@ -0,0 +1,17 @@ + .setcpu "6502" + .org $1000 +L1000: ldy #$28 + lda @L1017,y + sta $2000,y + dey + bpl L1000 + lda @L103F + lda @L103F+1 + lda done-1 + jmp done + +@L1017: .incbin "20300-1.bin" +@L103F: .incbin "20300sub/20300-2.bin" + +done: rts + diff --git a/SourceGen/SGTestData/Expected/20300-binary-include_cc65.cfg b/SourceGen/SGTestData/Expected/20300-binary-include_cc65.cfg new file mode 100644 index 0000000..95d28bc --- /dev/null +++ b/SourceGen/SGTestData/Expected/20300-binary-include_cc65.cfg @@ -0,0 +1,9 @@ +# 6502bench SourceGen generated linker script for 20300-binary-include +MEMORY { + MAIN: file=%O, start=%S, size=65536; +} +SEGMENTS { + CODE: load=MAIN, type=rw; +} +FEATURES {} +SYMBOLS {} diff --git a/SourceGen/SGTestData/Expected/20300-binary-include_merlin32.S b/SourceGen/SGTestData/Expected/20300-binary-include_merlin32.S new file mode 100644 index 0000000..8fc6ec0 --- /dev/null +++ b/SourceGen/SGTestData/Expected/20300-binary-include_merlin32.S @@ -0,0 +1,17 @@ + org $1000 +L1000 ldy #$28 + lda :L1017,y + sta $2000,y + dey + bpl L1000 + lda :L103F + lda :L103F+1 + lda done-1 + jmp done + +:L1017 hex 20212223242526272829303132333435363738394142434445464748494a5051 + hex 5253545556575859 +:L103F ds 32,$ff + +done rts + diff --git a/SourceGen/SGTestData/Source/20300-binary-include.S b/SourceGen/SGTestData/Source/20300-binary-include.S new file mode 100644 index 0000000..608d755 --- /dev/null +++ b/SourceGen/SGTestData/Source/20300-binary-include.S @@ -0,0 +1,31 @@ +; Copyright 2024 faddenSoft. All Rights Reserved. +; See the LICENSE.txt file for distribution terms (Apache 2.0). +; +; Assembler: Merlin 32 +; + + org $1000 +start ldy #40 + lda blob1,Y + sta $2000,Y + dey + bpl start + + lda blob2 + lda blob2+1 + lda blob2+31 + + jmp done + +; EDIT: set blob as binary include, current dir +blob1 + asc ' !"#$%&',27,'()' + asc '0123456789' + asc 'ABCDEFGHIJ' + asc 'PQRSTUVWXY' + +; EDIT: set blob as binary include, file in subdir +blob2 + ds 32,$ff + +done rts diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index c1afaac..0080c3a 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -74,6 +74,7 @@ + diff --git a/SourceGen/Tests/GenTest.cs b/SourceGen/Tests/GenTest.cs index 91488cb..02a3051 100644 --- a/SourceGen/Tests/GenTest.cs +++ b/SourceGen/Tests/GenTest.cs @@ -309,6 +309,26 @@ namespace SourceGen.Tests { //continue; } + // Generate binary includes. These are not verified in the "expected source" + // section because we'll do the necessary check in the binary diff. + if (!BinaryInclude.PrepareList(genResults.BinaryIncludes, workDir, + out string failMsg)) { + ReportErrMsg("Failed processing binary includes: " + failMsg); + ReportProgress("\r\n"); + didFail = true; + } else { + foreach (BinaryInclude.Excision exc in genResults.BinaryIncludes) { + if (!BinaryInclude.GenerateOutputFile(exc, project.FileData, + out string failMsg2)) { + ReportErrMsg("Failed processing binary include at +" + + exc.Offset.ToString("x6") + ": " + failMsg2); + ReportProgress("\r\n"); + didFail = true; + break; + } + } + } + // Assemble code. ReportProgress(" " + asmId.ToString() + " assemble..."); IAssembler asm = AssemblerInfo.GetAssembler(asmId); @@ -604,8 +624,8 @@ namespace SourceGen.Tests { /// Removes the contents of a temporary work directory. Only files that we believe /// to be products of the generator or assembler are removed. /// - /// - /// + /// Full pathname of work directory. + /// Test number, used to evaluate files for removal. private void ScrubWorkDirectory(string workDir, int testNum) { string checkString = testNum.ToString(); if (checkString.Length != 5) { @@ -613,6 +633,20 @@ namespace SourceGen.Tests { return; } + // Remove any subdirectories that match the pattern, e.g. for binary includes. + foreach (string pathName in Directory.EnumerateDirectories(workDir)) { + string fileName = Path.GetFileName(pathName); + if (fileName.Contains(checkString)) { + ScrubWorkDirectory(pathName, testNum); + try { + Directory.Delete(pathName); + } catch (Exception ex) { + ReportErrMsg("unable to remove dir '" + fileName + "': " + ex.Message); + } + } + } + + // Remove all matching files. foreach (string pathName in Directory.EnumerateFiles(workDir)) { bool doRemove = false; string fileName = Path.GetFileName(pathName); diff --git a/SourceGen/WeakSymbolRef.cs b/SourceGen/WeakSymbolRef.cs index edf789f..47943d9 100644 --- a/SourceGen/WeakSymbolRef.cs +++ b/SourceGen/WeakSymbolRef.cs @@ -79,13 +79,13 @@ namespace SourceGen { public bool IsVariable { get { return VarType != LocalVariableType.NotVar; } } /// - /// Constructor. + /// Standard constructor. /// public WeakSymbolRef(string label, Part part) : this(label, part, LocalVariableType.NotVar) { } /// - /// Constructor. + /// Constructor for local variable table references. /// public WeakSymbolRef(string label, Part part, LocalVariableType varType) { Debug.Assert(label != null); diff --git a/SourceGen/WpfGui/EditAppSettings.xaml b/SourceGen/WpfGui/EditAppSettings.xaml index 7d57a49..b1d8fae 100644 --- a/SourceGen/WpfGui/EditAppSettings.xaml +++ b/SourceGen/WpfGui/EditAppSettings.xaml @@ -653,6 +653,12 @@ limitations under the License. VerticalAlignment="Center" Margin="{StaticResource TBS}" Text=".placeho" MaxLength="12" FontFamily="{StaticResource GeneralMonoFont}"/> + + - + + + + + diff --git a/SourceGen/WpfGui/EditDataOperand.xaml.cs b/SourceGen/WpfGui/EditDataOperand.xaml.cs index 6eef077..ebb19eb 100644 --- a/SourceGen/WpfGui/EditDataOperand.xaml.cs +++ b/SourceGen/WpfGui/EditDataOperand.xaml.cs @@ -266,6 +266,12 @@ namespace SourceGen.WpfGui { UpdateControls(); } + private void BinaryIncludeTextBox_TextChanged(object sender, TextChangedEventArgs e) { + radioBinaryInclude.IsChecked = true; + // Update OK button based on filename 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. @@ -377,6 +383,10 @@ namespace SourceGen.WpfGui { } IsValid = isOk; + if (radioBinaryInclude.IsChecked == true) { + IsValid &= AsmGen.BinaryInclude.ValidatePathName(binaryIncludeTextBox.Text); + } + // If dense hex with a limit is selected, check the value. if (radioDenseHexLimited.IsChecked == true) { if (MaxDenseBytesPerLine > 0) { @@ -514,6 +524,9 @@ namespace SourceGen.WpfGui { radioFill.IsEnabled = false; } } + + // We can't handle multiple ranges because we need to set the filename. + radioBinaryInclude.IsEnabled = (mSelection.RangeCount == 1); } /// @@ -901,6 +914,11 @@ namespace SourceGen.WpfGui { case FormatDescriptor.Type.Junk: preferredFormat = radioJunk; break; + case FormatDescriptor.Type.BinaryInclude: + preferredFormat = radioBinaryInclude; + binaryIncludeTextBox.Text = + AsmGen.BinaryInclude.ConvertPathNameFromStorage(dfd.Extra); + break; default: // Should not be here. Debug.Assert(false); @@ -1077,6 +1095,10 @@ namespace SourceGen.WpfGui { type = FormatDescriptor.Type.Junk; JunkAlignmentItem comboItem = (JunkAlignmentItem)junkAlignComboBox.SelectedItem; subType = comboItem.FormatSubType; + } else if (radioBinaryInclude.IsChecked == true) { + type = FormatDescriptor.Type.BinaryInclude; + // path will be extracted directly by subroutine + Debug.Assert(mSelection.RangeCount == 1); } else if (radioStringMixed.IsChecked == true) { type = FormatDescriptor.Type.StringGeneric; subType = charSubType; @@ -1176,7 +1198,13 @@ namespace SourceGen.WpfGui { // 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) { + if (type == FormatDescriptor.Type.BinaryInclude) { + // Special case. We know there can be only one of these, so just grab the + // filename directly instead of passing it in as a rare argument. + string storePath = + AsmGen.BinaryInclude.ConvertPathNameToStorage(binaryIncludeTextBox.Text); + dfd = FormatDescriptor.Create(chunkLength, type, storePath); + } else if (subType == FormatDescriptor.SubType.Symbol) { dfd = FormatDescriptor.Create(chunkLength, symbolRef, type == FormatDescriptor.Type.NumericBE); } else { diff --git a/SourceGen/WpfGui/EditInstructionOperand.xaml.cs b/SourceGen/WpfGui/EditInstructionOperand.xaml.cs index 328830f..1f4bd0b 100644 --- a/SourceGen/WpfGui/EditInstructionOperand.xaml.cs +++ b/SourceGen/WpfGui/EditInstructionOperand.xaml.cs @@ -695,6 +695,7 @@ namespace SourceGen.WpfGui { case FormatDescriptor.Type.Fill: case FormatDescriptor.Type.Uninit: case FormatDescriptor.Type.Junk: + case FormatDescriptor.Type.BinaryInclude: default: // Unexpected; used to be data? break; diff --git a/docs/sgmanual/editors.html b/docs/sgmanual/editors.html index a30d65e..74f661e 100644 --- a/docs/sgmanual/editors.html +++ b/docs/sgmanual/editors.html @@ -273,6 +273,17 @@ you can mark it as Junk. If it appears to be adding bytes to reach a power-of-two address boundary, you can designate it as an alignment directive. If you have multiple regions selected, only the alignment options that work for all regions will be shown.

+

If you want to import a section of the file as a binary file, rather +than representing it in the assembly source, you can set the region as +a Binary Include. These sections must be for a single +unbroken section of the file. Assign a filename to use for the output +file. Filenames may be partial paths, but may not reference directories +above the project directory (with "..") or include double quotes (which +would require escaping in the assembler output). Each binary include +directive must output to a different filename (case-insensitive). During +assembly source generation, existing files will only be overwritten if +they have the same length as the binary include; if they have a different +length, an error will be reported.

The String items are enabled or disabled depending on whether the data you have selected is in the appropriate format. For example,