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,