Added file loading (bin, text files) into SCRIPTS
This commit is contained in:
parent
3748e0bd3e
commit
b2e9e3dc17
10 changed files with 694 additions and 4 deletions
5
examples/load_binary_demo/cm.sh
Executable file
5
examples/load_binary_demo/cm.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
# Define filename as variable
|
||||
PROGNAME="load_binary_demo"
|
||||
# Compile and assemble directly
|
||||
c65gm ${PROGNAME}.c65
|
||||
50
examples/load_binary_demo/load_binary_demo.c65
Normal file
50
examples/load_binary_demo/load_binary_demo.c65
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//-----------------------------------------------------------
|
||||
// load_binary Demo
|
||||
// Demonstrates loading binary data from a file at compile time
|
||||
// using the Starlark scripting built-in load_binary().
|
||||
//
|
||||
// The SCRIPT block reads screendata.bin (40 bytes of C64
|
||||
// screen codes) and emits them as assembler data. At runtime,
|
||||
// a c65gm FOR loop copies the data to screen memory ($0400)
|
||||
// using WORD pointers with self-modifying code.
|
||||
//
|
||||
// Build: c65gm load_binary_demo.c65
|
||||
//-----------------------------------------------------------
|
||||
|
||||
#INCLUDE <c64start.c65>
|
||||
|
||||
GOTO start
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Load binary data at compile time using Starlark load_binary()
|
||||
// and emit it as data statements in the assembly output.
|
||||
//-----------------------------------------------------------
|
||||
SCRIPT
|
||||
data = load_binary("screendata.bin")
|
||||
print("; Loaded %d bytes from screendata.bin" % len(data))
|
||||
print("scrdata:")
|
||||
for i in range(0, len(data), 8):
|
||||
row = ", ".join(["%d" % b for b in data[i:i+8]])
|
||||
print(" !8 " + row)
|
||||
ENDSCRIPT
|
||||
|
||||
LABEL start
|
||||
|
||||
// Pointers to source data and screen memory
|
||||
WORD src
|
||||
WORD dst
|
||||
BYTE i
|
||||
BYTE val
|
||||
|
||||
POINTER src TO scrdata
|
||||
POINTER dst TO $0400
|
||||
|
||||
// Copy 40 bytes from scrdata to screen memory
|
||||
FOR i = 0 TO 39
|
||||
val = PEEK src
|
||||
POKE dst WITH val
|
||||
src++
|
||||
dst++
|
||||
NEXT
|
||||
|
||||
SUBEND
|
||||
2
examples/load_binary_demo/screendata.bin
Normal file
2
examples/load_binary_demo/screendata.bin
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
!"#$%&'(
|
||||
1
examples/load_binary_demo/start_in_vice.sh
Normal file
1
examples/load_binary_demo/start_in_vice.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
x64 -autostartprgmode 1 load_binary_demo.prg
|
||||
|
|
@ -43,6 +43,11 @@ type CompilerContext struct {
|
|||
|
||||
// ScriptMacros holds named macro definitions from SCRIPT MACRO blocks
|
||||
ScriptMacros map[string]*ScriptMacro
|
||||
|
||||
// ProjectRoot is the absolute path of the directory containing the main input .c65 file.
|
||||
// Used by scripting built-ins (load_binary, load_text) to resolve relative file paths
|
||||
// and enforce security (no access outside project root).
|
||||
ProjectRoot string
|
||||
}
|
||||
|
||||
// NewCompilerContext creates a new compiler context with initialized resources
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"c65gm/internal/preproc"
|
||||
|
|
@ -213,9 +214,11 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
|
|||
// Set execution limit (prevent infinite loops)
|
||||
thread.SetMaxExecutionSteps(1000000) // 1M steps
|
||||
|
||||
// Build predeclared: math module + library globals
|
||||
// Build predeclared: math module + library globals + file I/O builtins
|
||||
predeclared := starlark.StringDict{
|
||||
"math": math.Module,
|
||||
"load_binary": makeLoadBinary(ctx.ProjectRoot),
|
||||
"load_text": makeLoadText(ctx.ProjectRoot),
|
||||
}
|
||||
for k, v := range ctx.ScriptLibraryGlobals {
|
||||
predeclared[k] = v
|
||||
|
|
@ -280,6 +283,121 @@ func expandVariables(text string, ctx *CompilerContext) string {
|
|||
return result
|
||||
}
|
||||
|
||||
// validateScriptFilePath checks that path is safe and resolves it within the project root.
|
||||
// It rejects absolute paths and path traversal (..).
|
||||
func validateScriptFilePath(projectRoot, path string) (string, error) {
|
||||
if projectRoot == "" {
|
||||
return "", fmt.Errorf("project root not set (internal error)")
|
||||
}
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("file path must not be empty")
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
return "", fmt.Errorf("absolute paths are not allowed: %s", path)
|
||||
}
|
||||
|
||||
// Reject path traversal components
|
||||
cleaned := filepath.Clean(path)
|
||||
for _, component := range strings.Split(cleaned, string(filepath.Separator)) {
|
||||
if component == ".." {
|
||||
return "", fmt.Errorf("path traversal is not allowed: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve against project root
|
||||
resolved := filepath.Join(projectRoot, cleaned)
|
||||
|
||||
// Verify containment within project root
|
||||
projectRootWithSep := projectRoot + string(filepath.Separator)
|
||||
if !strings.HasPrefix(resolved, projectRootWithSep) && resolved != projectRoot {
|
||||
return "", fmt.Errorf("file access denied: path resolves outside project folder")
|
||||
}
|
||||
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// makeLoadBinary creates a Starlark builtin function load_binary(path, offset?, length?)
|
||||
// that reads a binary file relative to the project root and returns a list of ints (0-255).
|
||||
func makeLoadBinary(projectRoot string) *starlark.Builtin {
|
||||
return starlark.NewBuiltin("load_binary", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
var path string
|
||||
var offset, length int
|
||||
if err := starlark.UnpackPositionalArgs("load_binary", args, kwargs, 1, &path, &offset, &length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolvedPath, err := validateScriptFilePath(projectRoot, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load_binary: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(resolvedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load_binary: cannot read %s: %w", path, err)
|
||||
}
|
||||
|
||||
if offset < 0 {
|
||||
return nil, fmt.Errorf("load_binary: offset must be non-negative, got %d", offset)
|
||||
}
|
||||
if offset > len(data) {
|
||||
return nil, fmt.Errorf("load_binary: offset %d exceeds file size %d", offset, len(data))
|
||||
}
|
||||
data = data[offset:]
|
||||
|
||||
if length > 0 {
|
||||
if length > len(data) {
|
||||
return nil, fmt.Errorf("load_binary: length %d exceeds available data %d", length, len(data))
|
||||
}
|
||||
data = data[:length]
|
||||
}
|
||||
|
||||
result := make([]starlark.Value, len(data))
|
||||
for i, b := range data {
|
||||
result[i] = starlark.MakeInt(int(b))
|
||||
}
|
||||
|
||||
return starlark.NewList(result), nil
|
||||
})
|
||||
}
|
||||
|
||||
// makeLoadText creates a Starlark builtin function load_text(path)
|
||||
// that reads a text file relative to the project root and returns a list of strings (lines).
|
||||
func makeLoadText(projectRoot string) *starlark.Builtin {
|
||||
return starlark.NewBuiltin("load_text", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
var path string
|
||||
if err := starlark.UnpackPositionalArgs("load_text", args, kwargs, 1, &path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolvedPath, err := validateScriptFilePath(projectRoot, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load_text: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(resolvedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load_text: cannot read %s: %w", path, err)
|
||||
}
|
||||
|
||||
text := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||
text = strings.TrimRight(text, "\n")
|
||||
|
||||
var lines []string
|
||||
if text == "" {
|
||||
lines = []string{}
|
||||
} else {
|
||||
lines = strings.Split(text, "\n")
|
||||
}
|
||||
|
||||
result := make([]starlark.Value, len(lines))
|
||||
for i, line := range lines {
|
||||
result[i] = starlark.String(line)
|
||||
}
|
||||
|
||||
return starlark.NewList(result), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ExecuteMacro executes a named macro with the given arguments and returns output lines
|
||||
func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]string, error) {
|
||||
// Look up the macro
|
||||
|
|
@ -331,9 +449,11 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
|||
// Set execution limit
|
||||
thread.SetMaxExecutionSteps(1000000)
|
||||
|
||||
// Build predeclared: math + library globals + parameter bindings
|
||||
// Build predeclared: math + library globals + file I/O builtins + parameter bindings
|
||||
predeclared := starlark.StringDict{
|
||||
"math": math.Module,
|
||||
"load_binary": makeLoadBinary(ctx.ProjectRoot),
|
||||
"load_text": makeLoadText(ctx.ProjectRoot),
|
||||
}
|
||||
for k, v := range ctx.ScriptLibraryGlobals {
|
||||
predeclared[k] = v
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -437,3 +438,407 @@ func TestPrintScriptErrorFallback(t *testing.T) {
|
|||
t.Errorf("output should contain the error message, got: %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for validateScriptFilePath ---
|
||||
|
||||
func TestValidateScriptFilePath_Valid(t *testing.T) {
|
||||
projectRoot := "/home/user/project"
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"file.bin", filepath.Join(projectRoot, "file.bin")},
|
||||
{"subdir/file.bin", filepath.Join(projectRoot, "subdir/file.bin")},
|
||||
{"./file.bin", filepath.Join(projectRoot, "file.bin")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := validateScriptFilePath(projectRoot, tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("validateScriptFilePath(%q, %q) unexpected error: %v", projectRoot, tt.path, err)
|
||||
continue
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("validateScriptFilePath(%q, %q) = %q, want %q", projectRoot, tt.path, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateScriptFilePath_RejectsAbsolute(t *testing.T) {
|
||||
_, err := validateScriptFilePath("/home/user/project", "/etc/passwd")
|
||||
if err == nil {
|
||||
t.Error("expected error for absolute path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateScriptFilePath_RejectsTraversal(t *testing.T) {
|
||||
projectRoot := "/home/user/project"
|
||||
tests := []string{
|
||||
"../secret.bin",
|
||||
"subdir/../../secret.bin",
|
||||
}
|
||||
|
||||
for _, path := range tests {
|
||||
_, err := validateScriptFilePath(projectRoot, path)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for traversal path: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateScriptFilePath_RejectsEmptyProjectRoot(t *testing.T) {
|
||||
_, err := validateScriptFilePath("", "file.bin")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty project root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateScriptFilePath_RejectsEmptyPath(t *testing.T) {
|
||||
_, err := validateScriptFilePath("/home/user/project", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty path")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for load_binary builtin ---
|
||||
|
||||
func TestExecuteScript_LoadBinary(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "data = load_binary(\"data.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
{Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
|
||||
{Text: "print(\"last=\" + str(data[7]))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 3 {
|
||||
t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=8" {
|
||||
t.Errorf("output[0] = %q, want \"len=8\"", output[0])
|
||||
}
|
||||
if output[1] != "first=0" {
|
||||
t.Errorf("output[1] = %q, want \"first=0\"", output[1])
|
||||
}
|
||||
if output[2] != "last=7" {
|
||||
t.Errorf("output[2] = %q, want \"last=7\"", output[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadBinary_Offset(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
// Load with offset 2: should return [0x02, 0x03, 0x04, 0x05]
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "data = load_binary(\"data.bin\", 2)", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
{Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 2 {
|
||||
t.Fatalf("expected at least 2 output lines, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=4" {
|
||||
t.Errorf("output[0] = %q, want \"len=4\"", output[0])
|
||||
}
|
||||
if output[1] != "first=2" {
|
||||
t.Errorf("output[1] = %q, want \"first=2\"", output[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadBinary_OffsetAndLength(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
// Load with offset 3, length 2: should return [0x03, 0x04]
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "data = load_binary(\"data.bin\", 3, 2)", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
{Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
|
||||
{Text: "print(\"last=\" + str(data[1]))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 3 {
|
||||
t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=2" {
|
||||
t.Errorf("output[0] = %q, want \"len=2\"", output[0])
|
||||
}
|
||||
if output[1] != "first=3" {
|
||||
t.Errorf("output[1] = %q, want \"first=3\"", output[1])
|
||||
}
|
||||
if output[2] != "last=4" {
|
||||
t.Errorf("output[2] = %q, want \"last=4\"", output[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadBinary_NonexistentFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "load_binary(\"nonexistent.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
_, err := executeScript(scriptLines, ctx, false)
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file")
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "Starlark error") {
|
||||
t.Errorf("error should contain 'Starlark error', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadBinary_TraversalRejected(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "load_binary(\"../outside.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
_, err := executeScript(scriptLines, ctx, false)
|
||||
if err == nil {
|
||||
t.Error("expected error for path traversal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadBinary_AbsoluteRejected(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "load_binary(\"/etc/passwd\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
_, err := executeScript(scriptLines, ctx, false)
|
||||
if err == nil {
|
||||
t.Error("expected error for absolute path")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for load_text builtin ---
|
||||
|
||||
func TestExecuteScript_LoadText(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
textContent := "hello\nworld\nfoo"
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
{Text: "print(\"line0=\" + lines[0])", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
|
||||
{Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
|
||||
{Text: "print(\"line2=\" + lines[2])", Filename: "test.c65", LineNo: 5, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 4 {
|
||||
t.Fatalf("expected at least 4 output lines, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=3" {
|
||||
t.Errorf("output[0] = %q, want \"len=3\"", output[0])
|
||||
}
|
||||
if output[1] != "line0=hello" {
|
||||
t.Errorf("output[1] = %q, want \"line0=hello\"", output[1])
|
||||
}
|
||||
if output[2] != "line1=world" {
|
||||
t.Errorf("output[2] = %q, want \"line1=world\"", output[2])
|
||||
}
|
||||
if output[3] != "line2=foo" {
|
||||
t.Errorf("output[3] = %q, want \"line2=foo\"", output[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadText_TrailingNewline(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
textContent := "hello\nworld\n"
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 1 {
|
||||
t.Fatalf("expected at least 1 output line, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=2" {
|
||||
t.Errorf("output[0] = %q, want \"len=2\"", output[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadText_EmptyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "empty.txt"), []byte{}, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "lines = load_text(\"empty.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 1 {
|
||||
t.Fatalf("expected at least 1 output line, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=0" {
|
||||
t.Errorf("output[0] = %q, want \"len=0\"", output[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadText_CRLF(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
textContent := "hello\r\nworld\r\n"
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
|
||||
{Text: "print(\"line0=\" + lines[0])", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
|
||||
{Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 3 {
|
||||
t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=2" {
|
||||
t.Errorf("output[0] = %q, want \"len=2\"", output[0])
|
||||
}
|
||||
if output[1] != "line0=hello" {
|
||||
t.Errorf("output[1] = %q, want \"line0=hello\"", output[1])
|
||||
}
|
||||
if output[2] != "line1=world" {
|
||||
t.Errorf("output[2] = %q, want \"line1=world\"", output[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScript_LoadText_LibraryFunction(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
textContent := "foo\nbar\nbaz"
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pragma := preproc.NewPragma()
|
||||
ctx := NewCompilerContext(pragma)
|
||||
ctx.ProjectRoot = tmpDir
|
||||
|
||||
// Define a library function that uses load_text
|
||||
libLines := []preproc.Line{
|
||||
{Text: "def get_lines():\n return load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.ScriptLibrary},
|
||||
}
|
||||
_, err := executeScript(libLines, ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("library definition failed: %v", err)
|
||||
}
|
||||
|
||||
// Use the library function from a regular script
|
||||
scriptLines := []preproc.Line{
|
||||
{Text: "lines = get_lines()", Filename: "test.c65", LineNo: 5, Kind: preproc.Script},
|
||||
{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 6, Kind: preproc.Script},
|
||||
{Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 7, Kind: preproc.Script},
|
||||
}
|
||||
|
||||
output, err := executeScript(scriptLines, ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("executeScript failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) < 2 {
|
||||
t.Fatalf("expected at least 2 output lines, got %d: %v", len(output), output)
|
||||
}
|
||||
if output[0] != "len=3" {
|
||||
t.Errorf("output[0] = %q, want \"len=3\"", output[0])
|
||||
}
|
||||
if output[1] != "line1=bar" {
|
||||
t.Errorf("output[1] = %q, want \"line1=bar\"", output[1])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
language.md
52
language.md
|
|
@ -507,6 +507,58 @@ SCRIPT
|
|||
ENDSCRIPT
|
||||
```
|
||||
|
||||
#### File I/O
|
||||
|
||||
Scripts can read binary and text files at compile time using `load_binary()` and `load_text()`. These functions only allow access to files within the project folder (where the main .c65 file resides) and its subdirectories. Absolute paths and path traversal (`..`) are rejected.
|
||||
|
||||
**`load_binary(path, offset=0, length=0)`**
|
||||
|
||||
Reads a binary file and returns a list of integers (0-255). The optional `offset` parameter skips bytes at the start, and `length` limits how many bytes to read (0 = read to end of file).
|
||||
|
||||
```c65
|
||||
SCRIPT
|
||||
sprite = load_binary("assets/hero.spr")
|
||||
print("hero_sprite:")
|
||||
for i in range(0, len(sprite), 8):
|
||||
row = ", ".join(["$%02x" % b for b in sprite[i:i+8]])
|
||||
print(" !byte " + row)
|
||||
ENDSCRIPT
|
||||
```
|
||||
|
||||
**`load_text(path)`**
|
||||
|
||||
Reads a text file and returns a list of strings, one per line. Handles both `\n` (Unix) and `\r\n` (Windows) line endings.
|
||||
|
||||
```c65
|
||||
SCRIPT
|
||||
level = load_text("levels/lvl1.txt")
|
||||
print("level_map:")
|
||||
for y in range(len(level)):
|
||||
print(" !text " + repr(level[y]))
|
||||
ENDSCRIPT
|
||||
```
|
||||
|
||||
**Example — resource loading with SCRIPT LIBRARY:**
|
||||
|
||||
```c65
|
||||
SCRIPT LIBRARY
|
||||
# Load resources into library globals
|
||||
font_data = load_binary("assets/font.chr")
|
||||
level = load_text("levels/lvl1.txt")
|
||||
|
||||
def emit_font():
|
||||
print("font:")
|
||||
for i in range(0, len(font_data), 8):
|
||||
row = ", ".join(["$%02x" % b for b in font_data[i:i+8]])
|
||||
print(" !byte " + row)
|
||||
|
||||
def emit_level():
|
||||
print("level:")
|
||||
for line in level:
|
||||
print(" !text " + repr(line))
|
||||
ENDSCRIPT
|
||||
```
|
||||
|
||||
### SCRIPT LIBRARY Blocks
|
||||
|
||||
Define reusable Starlark functions that persist across all subsequent SCRIPT blocks:
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -139,6 +139,11 @@ func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes
|
|||
|
||||
// Create compiler and register commands
|
||||
comp := compiler.NewCompiler(pragma)
|
||||
absInput, err := filepath.Abs(inFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve input file path: %w", err)
|
||||
}
|
||||
comp.Context().ProjectRoot = filepath.Dir(absInput)
|
||||
comp.CmdlineOpt = opt
|
||||
comp.CmdlineDebug = optDebug
|
||||
// Build I/O regions from CLI flags
|
||||
|
|
|
|||
45
syntax.md
45
syntax.md
|
|
@ -397,6 +397,51 @@ ENDSCRIPT
|
|||
- Maximum 1 million execution steps (prevents infinite loops)
|
||||
- Executed at compile time, not runtime
|
||||
|
||||
**File I/O Built-in Functions:**
|
||||
|
||||
Scripts can read files from the project folder (the directory containing the main input .c65 file) using the following built-in functions:
|
||||
|
||||
- **`load_binary(path, offset=0, length=0)`** — Loads a binary file and returns a list of integers (0-255). Optional `offset` specifies a starting byte position, and `length` limits the number of bytes read (0 = read all remaining bytes).
|
||||
- **`load_text(path)`** — Loads a text file and returns a list of strings, one per line. Handles both `\n` and `\r\n` line endings.
|
||||
|
||||
**Security restrictions:**
|
||||
- Only relative paths are allowed (no absolute paths like `/etc/passwd`)
|
||||
- Path traversal (`..`) is not permitted
|
||||
- Only files inside the project folder (or its subdirectories) can be accessed
|
||||
- File access errors cause a compile error with source location
|
||||
|
||||
**Example — loading sprite data:**
|
||||
|
||||
```
|
||||
SCRIPT
|
||||
sprite = load_binary("assets/hero.spr") # load binary data
|
||||
print("hero_sprite:")
|
||||
for i in range(0, len(sprite), 8):
|
||||
row = ", ".join(["$%02x" % b for b in sprite[i:i+8]])
|
||||
print(" !byte " + row)
|
||||
ENDSCRIPT
|
||||
```
|
||||
|
||||
**Example — loading a level map:**
|
||||
|
||||
```
|
||||
SCRIPT LIBRARY
|
||||
# Load a text-based level map at compile time
|
||||
level = load_text("levels/lvl1.txt")
|
||||
height = len(level)
|
||||
width = len(level[0]) if height > 0 else 0
|
||||
|
||||
def emit_map():
|
||||
print("level_map:")
|
||||
for y in range(height):
|
||||
print(" !text " + repr(level[y]))
|
||||
print("level_height:")
|
||||
print(" !8 %d" % height)
|
||||
print("level_width:")
|
||||
print(" !8 %d" % width)
|
||||
ENDSCRIPT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCRIPT LIBRARY...ENDSCRIPT
|
||||
|
|
|
|||
Loading…
Reference in a new issue