Skip to content

Commit

Permalink
Binary includes
Browse files Browse the repository at this point in the history
This adds a new data format option, "binary include", that takes a
filename operand.  When assembly sources are generated, the section
of file is replaced with an appropriate pseudo-op, and binary files
are generated that hold the file contents.  This is a convenient way
to remove large binary blobs, such as music or sound samples, that
aren't useful to have in text form in the sources.

Partial pathnames are allowed, so you can output a sound blob to
"sounds/blather.bin".  For safety reasons, we don't allow the files
to be created above the project directory, and existing files will
only be overwritten if they have a matching length (so you don't
accidentally stomp on your project file).

The files are not currently shown in the GenAsm dialog, which lets
you see a preview of the generated sources.  The hex dump tool
can do this for the (presumably rare) situations where it's useful.

A new regression test, 20300-binary-include, has been added.  The
pseudo-op name can be overridden on-screen in the settings.

We don't currently do anything new for text/HTML exports.  It might
be useful to generate an optional appendix with a hex dump of the
excised sections.

(issue #144)
  • Loading branch information
fadden committed May 31, 2024
1 parent fa5d9ba commit 4e5c34f
Show file tree
Hide file tree
Showing 31 changed files with 668 additions and 19 deletions.
3 changes: 2 additions & 1 deletion PluginCommon/Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,8 @@ public enum DataType {
Dense,
Fill,
Uninit,
Junk
Junk,
BinaryInclude
}

/// <summary>
Expand Down
14 changes: 13 additions & 1 deletion SourceGen/AsmGen/AsmAcme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public class GenAcme : IGenerator {
// IGenerator
public int StartOffset { get { return 0; } }

/// <summary>
/// List of binary include sections found in the project.
/// </summary>
private List<BinaryInclude.Excision> mBinaryIncludes = new List<BinaryInclude.Excision>();

/// <summary>
/// Working directory, i.e. where we write our output file(s).
/// </summary>
Expand Down Expand Up @@ -139,6 +144,7 @@ private enum OutputMode {
{ "Uninit", "!skip" },
//Junk
{ "Align", "!align" },
{ "BinaryInclude", "!binary" },
{ "StrGeneric", "!text" }, // can use !xor for high ASCII
//StrReverse
//StrNullTerm
Expand Down Expand Up @@ -303,7 +309,7 @@ public GenerationResults GenerateSource(BackgroundWorker worker) {
}
mOutStream = null;

return new GenerationResults(pathNames, string.Empty);
return new GenerationResults(pathNames, string.Empty, mBinaryIncludes);
}

/// <summary>
Expand Down Expand Up @@ -483,6 +489,12 @@ public void OutputDataOp(int offset) {
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:
Expand Down
15 changes: 14 additions & 1 deletion SourceGen/AsmGen/AsmCc65.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public class GenCc65 : IGenerator {
// IGenerator
public int StartOffset { get { return 0; } }

/// <summary>
/// List of binary include sections found in the project.
/// </summary>
private List<BinaryInclude.Excision> mBinaryIncludes = new List<BinaryInclude.Excision>();

/// <summary>
/// Working directory, i.e. where we write our output file(s).
/// </summary>
Expand Down Expand Up @@ -138,6 +143,8 @@ public class GenCc65 : IGenerator {
{ "Dense", ".byte" }, // really just just comma-separated bytes
{ "Uninit", ".res" },
//Junk
//Align
{ "BinaryInclude", ".incbin" },
{ "StrGeneric", ".byte" },
//StrReverse
{ "StrNullTerm", ".asciiz" },
Expand Down Expand Up @@ -262,7 +269,7 @@ public GenerationResults GenerateSource(BackgroundWorker worker) {
}
mOutStream = null;

return new GenerationResults(pathNames, string.Empty);
return new GenerationResults(pathNames, string.Empty, mBinaryIncludes);
}

private void GenerateLinkerScript(StreamWriter sw) {
Expand Down Expand Up @@ -470,6 +477,12 @@ public void OutputDataOp(int offset) {
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:
Expand Down
5 changes: 4 additions & 1 deletion SourceGen/AsmGen/AsmMerlin32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public class GenMerlin32 : IGenerator {
{ "Uninit", "ds" },
//Junk
//Align
//BinaryInclude
{ "StrGeneric", "asc" },
{ "StrReverse", "rev" },
//StrNullTerm
Expand Down Expand Up @@ -243,7 +244,8 @@ public GenerationResults GenerateSource(BackgroundWorker worker) {
}
mOutStream = null;

return new GenerationResults(pathNames, string.Empty);
return new GenerationResults(pathNames, string.Empty,
new List<BinaryInclude.Excision>());
}

// IGenerator
Expand Down Expand Up @@ -316,6 +318,7 @@ public void OutputDataOp(int offset) {
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;
Expand Down
15 changes: 14 additions & 1 deletion SourceGen/AsmGen/AsmTass64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,18 @@ public class GenTass64 : IGenerator {
// IGenerator
public LabelLocalizer Localizer { get { return mLocalizer; } }

// IGenerator
public int StartOffset {
get {
return mHasPrgHeader ? 2 : 0;
}
}

/// <summary>
/// List of binary include sections found in the project.
/// </summary>
private List<BinaryInclude.Excision> mBinaryIncludes = new List<BinaryInclude.Excision>();

/// <summary>
/// Working directory, i.e. where we write our output file(s).
/// </summary>
Expand Down Expand Up @@ -158,6 +164,7 @@ private enum OutputMode {
{ "Uninit", ".fill" },
//Junk
{ "Align", ".align" },
{ "BinaryInclude", ".binary" },
{ "StrGeneric", ".text" },
//StrReverse
{ "StrNullTerm", ".null" },
Expand Down Expand Up @@ -323,7 +330,7 @@ public GenerationResults GenerateSource(BackgroundWorker worker) {
}
mOutStream = null;

return new GenerationResults(pathNames, extraOptions);
return new GenerationResults(pathNames, extraOptions, mBinaryIncludes);
}

// IGenerator
Expand Down Expand Up @@ -577,6 +584,12 @@ public void OutputDataOp(int offset) {
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:
Expand Down
225 changes: 225 additions & 0 deletions SourceGen/AsmGen/BinaryInclude.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
/// Helper functions for working with binary includes.
/// </summary>
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

/// <summary>
/// Class to help when gathering up binary includes during asm gen.
/// </summary>
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 + "\"]";
}
}

/// <summary>
/// Determines the full path of each binary include output file. Checks for duplicates.
/// Sorts the list by case-insensitive pathname.
/// </summary>
/// <param name="list">List of binary include excisions.</param>
/// <param name="workDir">Working directory.</param>
/// <param name="failMsg">On failure, a human-readable error message.</param>
/// <returns>True on success.</returns>
public static bool PrepareList(List<Excision> 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;
}

/// <summary>
/// Generates the output file with the binary include data.
/// </summary>
/// <param name="exc">Binary include object, with full pathname computed.</param>
/// <param name="data">Project data array.</param>
/// <param name="failMsg">On failure, a human-readable error message.</param>
/// <returns>True on success.</returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// <para>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.)</para>
/// <para>We screen the filename for illegal characters, though what works on one
/// platform might not on another. We can't guarantee validity.</para>
/// </remarks>
/// <param name="pathName">Partial path to verify.</param>
/// <returns>True if the path looks correct.</returns>
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;
}

/// <summary>
/// Converts a binary include pathname to a format suited for storage.
/// </summary>
/// <param name="pathName">Partial pathname.</param>
/// <returns>String to store.</returns>
public static string ConvertPathNameToStorage(string pathName) {
return PATH_PREFIX_CHAR + pathName;
}

/// <summary>
/// Converts the stored name back to a path prefix string.
/// </summary>
/// <param name="storageStr">Stored string.</param>
/// <returns>Path prefix.</returns>
public static string ConvertPathNameFromStorage(string storageStr) {
if (string.IsNullOrEmpty(storageStr) || storageStr[0] != PATH_PREFIX_CHAR) {
return "!BAD STORED NAME!";
}
return storageStr.Substring(1);
}
}
}
Loading

0 comments on commit 4e5c34f

Please sign in to comment.