See More

""" 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)