"""
PC-BASIC - program.py
Program buffer utilities
(c) 2013--2023 Rob Hagemans
This file is released under the GNU GPL version 3 or later.
"""
import binascii
import logging
import struct
import io
from ..compat import int2byte
from .base import error
from .base import tokens as tk
from . import values
from . import converter
class Program(object):
"""BASIC program."""
def __init__(self, tokeniser, lister, hide_listing,
allow_protect, allow_code_poke, memory, bytecode, rebuild_offsets):
"""Initialise program."""
self._memory = memory
# program bytecode buffer
self.bytecode = bytecode
self.erase()
self.max_list_line = hide_listing if hide_listing else 65535
self.allow_protect = allow_protect
self.allow_code_poke = allow_code_poke
self._rebuild_offsets = rebuild_offsets
# to be set when file memory is initialised
self.code_start = memory.code_start
# for detokenise_line()
self.tokeniser = tokeniser
self.lister = lister
def __repr__(self):
"""Return a marked-up hex dump of the program (for debugging)."""
code = self.bytecode.getvalue()
offset_val, p = 0, 0
output = []
for key in sorted(self.line_numbers.keys())[1:]:
next_p = self.line_numbers[key]
offset, linum = code[p+1:p+3], code[p+3:p+5]
last_offset = offset_val
offset_val = struct.unpack(' pre:
pre = linum
return pre
def rebuild_line_dict(self):
"""Preparse to build line number dictionary."""
self.line_numbers, offsets = {}, []
self.bytecode.seek(0)
scanline, scanpos, last = 0, 0, 0
while True:
# pass \x00
self.bytecode.read(1)
scanline = self.lister.detokenise_line_number(self.bytecode)
if scanline == -1:
scanline = 65536
# if detokenise_line_number returns -1, it leaves the stream pointer here:
# 00 _00_ 00 1A
break
self.line_numbers[scanline] = scanpos
last = scanpos
self.bytecode.skip_to(tk.END_LINE)
scanpos = self.bytecode.tell()
offsets.append(scanpos)
self.line_numbers[65536] = scanpos
# rebuild offsets
if self._rebuild_offsets:
self.bytecode.seek(0)
last = 0
for pos in offsets:
self.bytecode.read(1)
self.bytecode.write(struct.pack(' self._memory.stack_start():
raise error.BASICError(error.OUT_OF_MEMORY)
self.bytecode.write(
struct.pack('= fromline and num <= toline ]
beyond = [num for num in self.line_numbers if num > toline ]
# find lowest number strictly above range
afterpos = self.line_numbers[min(beyond)]
# find lowest number within range
try:
startpos = self.line_numbers[min(deleteable)]
except ValueError:
startpos = afterpos
return startpos, afterpos, deleteable, beyond
def delete(self, fromline, toline):
"""Delete range of lines from stored program."""
fromline, toline = self.explicit_lines(fromline, toline)
fromline = fromline if fromline is not None else min(self.line_numbers)
toline = toline if toline is not None else 65535
startpos, afterpos, deleteable, beyond = self.find_pos_line_dict(fromline, toline)
if not deleteable:
# no lines selected
raise error.BASICError(error.IFC)
# do the delete
self.bytecode.seek(afterpos)
rest = self.bytecode.read()
self.bytecode.seek(startpos)
self.truncate(rest)
# update line number dict
self.update_line_dict(startpos, afterpos, 0, deleteable, beyond)
def edit(self, console, from_line, target_bytepos):
"""Output program line to console and position cursor."""
if self.protected:
console.write(b'%d\r' % (from_line,))
raise error.BASICError(error.IFC)
# list line
self.bytecode.seek(self.line_numbers[from_line]+1)
_, output, byte_to_text_positions = self.lister.detokenise_line(self.bytecode)
# find text position from byte position
if target_bytepos is None:
textpos = None
else:
textpos = min(
_textpos
for _bytepos, _textpos in byte_to_text_positions
if target_bytepos <= _bytepos
)
# no newline to avoid scrolling on line 24
console.list_line(bytes(output), newline=False, set_text_position=textpos)
def renum(self, console, new_line, start_line, step):
"""Renumber stored program."""
new_line = 10 if new_line is None else new_line
start_line = 0 if start_line is None else start_line
step = 10 if step is None else step
# ensure we're not about to overwrite anything
remaining = [_k for _k in self.line_numbers.keys() if _k < start_line]
if remaining and new_line <= max(remaining):
raise error.BASICError(error.IFC)
# get a sorted list of line numbers
# assign the new numbers
old_to_new = {}
for old_line in sorted(_k for _k in self.line_numbers.keys() if _k >= start_line):
if old_line < 65535 and new_line > 65529:
raise error.BASICError(error.IFC)
if old_line == 65536:
break
old_to_new[old_line] = new_line
self.last_stored = new_line
new_line += step
# write the new numbers
for old_line in old_to_new:
self.bytecode.seek(self.line_numbers[old_line])
# skip the \x00\xC0\xDE & overwrite line number
self.bytecode.read(3)
self.bytecode.write(struct.pack(' 255 chars
raise error.BASICError(error.LINE_BUFFER_OVERFLOW)
linebuf = self.tokeniser.tokenise_line(line)
if linebuf.read(1) == b'\0':
# line starts with a number, add to program memory; store_line seeks to 1 first
self.store_line(linebuf)
else:
# we have read the :
if linebuf.skip_blank() not in tk.END_LINE:
raise error.BASICError(error.DIRECT_STATEMENT_IN_FILE)
def save(self, g):
"""Save the program to stream g in (A)scii, (B)ytecode or (P)rotected mode."""
mode = g.filetype
if self.protected and mode != b'P':
raise error.BASICError(error.IFC)
current = self.bytecode.tell()
# skip first \x00 in bytecode
self.bytecode.seek(1)
if mode == b'B':
# binary bytecode mode
g.write(self.bytecode.read())
elif mode == b'P':
# protected mode
converter.protect(self.bytecode, g)
else:
# ascii mode
while True:
current_line, output, _ = self.lister.detokenise_line(self.bytecode)
if current_line == -1 or (current_line > self.max_list_line):
break
g.write_line(bytes(output))
self.bytecode.seek(current)
def list_lines(self, from_line, to_line):
"""List line range."""
from_line, to_line = self.explicit_lines(from_line, to_line)
if self.protected:
# don't list protected files
raise error.BASICError(error.IFC)
# 65529 is max insertable line number for GW-BASIC 3.23.
# however, 65530-65535 are executed if present in tokenised form.
# in GW-BASIC, 65530 appears in LIST, 65531 and above are hidden
if to_line is None:
to_line = self.max_list_line
numbers = [
num for num in self.line_numbers
if (from_line is None or num >= from_line) and num <= to_line
]
# sort by positions, not line numbers!
listable = sorted([self.line_numbers[num] for num in numbers])
if numbers:
self.last_stored = max(numbers)
lines = []
for pos in listable:
self.bytecode.seek(pos + 1)
_, line, _ = self.lister.detokenise_line(self.bytecode)
lines.append(bytes(line))
return lines
def get_memory(self, offset):
"""Retrieve data from program code."""
offset -= self.code_start
code = self.bytecode.getvalue()
try:
return ord(code[offset:offset+1])
except IndexError: # pragma: no cover
# variable memory starts immediately after the stored program
# ao memory access beyond the size of the program should not arrive here
logging.debug('PEEK beyond program size handled by Program class.')
return -1
def get_memory_block(self, offset, length):
"""Retrieve block of data from program code."""
offset -= self.code_start
code = self.bytecode.getvalue()
return bytearray(code[offset:offset+length])
def set_memory(self, offset, val):
"""Change program code."""
if not self.allow_code_poke:
logging.warning('Ignored POKE into program code')
return
offset -= self.code_start
loc = self.bytecode.tell()
# move pointer to end
self.bytecode.seek(0, 2)
if offset > self.bytecode.tell(): # pragma: no cover
# variable memory starts immediately after the stored program
# ao memory access beyond the size of the program should not arrive here
logging.debug('POKE beyond program size handled by Program class.')
else:
self.bytecode.seek(offset)
self.bytecode.write(int2byte(val))
# move pointer to end
self.bytecode.seek(0, 2)
self.rebuild_line_dict()
# restore program pointer
self.bytecode.seek(loc)