Frecon code: db.pal
From PROSE Programming Language - Wiki
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Database Routines for Frecon % Author: Mark R. Bannister % Date: 7/Oct/2017 % % Provides routines for creating and maintaining a database % of fixed length fields for the File Reconnaissance Tool % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ~Module EQUS {[frecon]} ._init attr/load P0, [psString], P1, [psInteger], P2, [psPointer], P3, [psBoolean] func/def [db_load], &[.db_load] func/def [db_open], &[.db_open], P1, P0, [dir], P0, [base] func/def [db_new_scanpath], &[.db_new_scanpath] func/def [db_set_scanpath], &[.db_set_scanpath], NULL, P0, [path] func/def [db_stat_all_records], &[.db_stat_all_records] func/def [db_obj_all_records], &[.db_obj_all_records] func/def [db_write_record], &[.db_write_record], NULL, P2, [file], P1, [recno], P3, [cksum] func/def [db_num_records], &[.db_num_records], P1 func/def [dump], &[.db_dump] func/def [db_truncate], &[.db_truncate] var/global NULL, P0, [db_name], [frecon.dat] % database filename var/global NULL, P2, [db_handle] % open file handle error/def [BadDatabase], [Database file corrupted] local/rtn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Load or create new database % % File format is as follows: % % Bytes 0-9, magic header % % 1 to n records, each 375 bytes long, 1st record is path to scan % A record consists of: % % Bytes +0x000 to +0x0FF (256 bytes) : Path name % Bytes +0x100 to +0x101 ( 2 bytes) : File mode % Bytes +0x102 to +0x109 ( 8 bytes) : File size in bytes % Bytes +0x10A to +0x10D ( 4 bytes) : Owner UID % Bytes +0x10E to +0x12B ( 30 bytes) : Owner name % Bytes +0x12C to +0x12F ( 4 bytes) : Group GID % Bytes +0x130 to +0x14D ( 30 bytes) : Group name % Bytes +0x14E to +0x155 ( 8 bytes) : File modification time % Bytes +0x156 to +0x175 ( 32 bytes) : SHA256 checksum (regular files only) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_load % % Set-up pointers to commonly used objects and attributes % reg/load P14, ![.prose.sys.io.stdin], P15, ![.prose.sys.io.stdout] attr/load P12, [psStreamIn], P13, [psStreamOut] % % Display help message and prompt % reg/load P0, (&[~help]) reg/load P0, (P0) var/addr P1, [db_name] attr/copy P1, P1, [psString] reg/copy PUSH, P1 attr/mvadd P15, P13, P0, [[], P1, [\] ] % % Collect user input % .get_stdin attr/copy P0, P14, P12 reg/jmpeq &[.get_stdin], P0, NULL % Ignore Ctrl+D reg/jmpeq &[.use_default], P0, [\n] % ENTER pressed to accept default % % Strip newline % reg/xscan P1, P0, [\n] reg/save P0, (P1) local/jmp &[.split_path] .use_default stack/pull P0 .split_path % % Filename is in P0, now split into dirname and basename components % reg/xscan P1, P0, [/] reg/jmpeq &[.no_dirname], P1, NULL .find_last_slash op/incr P1 reg/xscan P2, P0, [/], P1 reg/jmpeq &[.split], P2, NULL reg/move P1, P2 local/jmp &[.find_last_slash] .split reg/copy P2, (P0, #0, P1) % dirname reg/copy P0, (P0, P1) % basename local/jmp &[.call_db_open] .no_dirname reg/load P2, [.] .call_db_open % % Open database file % func/call P1, [db_open], P2, P0 % % If file is zero bytes long, or only contains the magic header, % then prompt for path to scan, otherwise we can read the path % from the database file % reg/jmplt &[.prompt_scanpath], P1, #0x181 % 0x177 + 10 is min. length func/call NULL, [db_stat_all_records] func/rtn .prompt_scanpath func/call NULL, [db_new_scanpath] func/rtn ~help EQUS {[ The Frecon database will contain all of the pathnames and statistics for all files underneath a given path. You can specify the location of an existing database that has been created previously by this program, or specify the location of a new database that you wish to create. By default, the database will reside in the current working diectory, but you may specify a relative or absolute path if you wish for it to reside in an alternative location. Name of database to load or create, or press ENTER to accept the default. ]} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Open the database for update % % As well as preparing for our first use of a given database file, % this will also prove that we have read and write access to it % % Returns size of current database file % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_open % % Read dirname and basename values from 1st and 2nd parameters % var/addr P10, [dir], P11, [base] attr/load P12, [psString], P13, [psStreamReadSize] attr/copy P10, P10, P12 attr/copy P11, P11, P12 % % Set-up the top of a new virtual filesystem branch to point to dirname % obj/def P0 class/add P0, [psFileHook] attr/add P0, [psFilePath], P10 obj/commitref P0, ![-], [fs_base], P0 % % Open file for update and place an exclusive advisory lock on it % obj/def P1 class/add P1, [psFile], [psStatUnix], [psStreamOpen], [psStreamSeek], [psFileLock] attr/add P1, [psStreamMode], [r+b], [psLockType], [Exclusive], [psBlocking], [TRUE], [psBlockingTimeout], #5, P13, #0x177 % default record size obj/commitref P1, P0, P11, P1 % % Store file handle in global variable % var/addr P0, [db_handle] attr/add P0, [psPointer], P1 % % Read size of file from stat attribute % attr/xcopy P2, P1, [statSize] % % If database file is greater than zero bytes, check that the file % size is a whole number of records % reg/jmpeq &[.return_filesize], P2, #0 attr/def P3, [psInteger], P2 opx/sub P3, P3, #10 % take off length of magic header opx/mod P3, P3, #0x177 % divide by record size reg/jmpeq &[.check_magic], P3, #0 % should be no remainder error/now ![.prose.error.frecon.BadDatabase], [File size incorrect, records truncated] .check_magic % % Check for magic header % attr/mod P1, P13, #10 % magic header is only 10 bytes attr/copy P3, P1, [psByteStream] attr/mod P1, P13, #0x177 % back to default record size reg/jmpeq &[.return_filesize], P3, [frecondb01] error/now ![.prose.error.frecon.BadDatabase], [Magic header bytes unrecognised] .return_filesize % % Return file size % func/rtn P2 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Close the database by deleting the global filehandle % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_close var/addr P0, [db_handle] obj/del P0 func/rtn ~help_new_path EQUS {[\nAll files and directories underneath a given path will be scanned and their vital statistics stored in the database. This information may then be queried and compared.\n\n]} ~prompt_new_path EQUS {[Name of path to scan, or press ENTER for current directory.\n]} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Prompt for path to scan and store this at the top of a new DB file % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_new_scanpath % % Set-up pointers to commonly used objects and attributes % reg/load P14, ![.prose.sys.io.stdin], P15, ![.prose.sys.io.stdout] attr/load P12, [psStreamIn], P13, [psStreamOut] % % Display help message and prompt % reg/load P0, (&[~help_new_path]) reg/load P0, (P0) attr/add P15, P13, P0 .path_prompt reg/load P0, (&[~prompt_new_path]) reg/load P0, (P0) attr/mvadd P15, P13, P0, [[.\] ] % % Collect user input % .get_stdin2 attr/copy P0, P14, P12 reg/jmpeq &[.get_stdin2], P0, NULL % Ignore Ctrl+D reg/jmpeq &[.use_cwd], P0, [\n] % ENTER pressed to accept default % % Strip newline % reg/xscan P1, P0, [\n] reg/save P0, (P1) local/jmp &[.check_path] .use_cwd reg/copy P0, [.] .check_path % % Check path is not too long and that it exists % reg/xload P2, (P0) reg/jmpgt &[.path_too_long], P2, #256 reg/copy P2, P0 error/jmp &[.no_such_file], ![.prose.error.sys.NoEntry] func/bcall P1, [scan_opendir], P2 error/jmp % % Make sure path is to a directory, not a file % attr/addr P1, P1 attr/index A, P1, [statMode] opa/and #040000 reg/jmpne &[.not_directory], A, #040000 % % Save path as first record in database file % var/addr P2, [db_handle] attr/addr P2, P2, [psPointer] attr/add P2, P13, [frecondb01] % magic header (10 bytes) reg/load P2, P1 % duplicate directory handle func/bcall NULL, [db_write_record], P2, #0, #0 func/call NULL, [scan_closedir], P1 func/rtn .path_too_long attr/add P15, P13, [\nPath too long (256 bytes max. permitted)\n] local/jmp &[.path_prompt] .no_such_file error/clr attr/add P15, P13, [\nPath not found, please try again\n] local/jmp &[.path_prompt] .not_directory func/bcall NULL, [scan_closedir], P1 attr/add P15, P13, [\nPath is not a directory, please try again\n] local/jmp &[.path_prompt] %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Read all records from DB file and gather statistics % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_stat_all_records func/call NULL, [stat_reset] % reset statistics var/addr P0, [db_handle] attr/addr P0, P0, [psPointer] attr/load P13, [psByteStream], P14, [psTime], P15, [psInteger] .read_loop attr/copy P1, P0, P13 reg/jmpeq &[.read_end], P1, NULL local/jsr &[.extract_fields] reg/cmp A, #0 eq: local/jsr &[.set_scanpath] % first record is scan path eq: reg/copy P11, P2 % copy base path into P11 func/bcall NULL, [stat_record], P2, P3, P4, P5, P6, P7, P8, P9 local/jmp &[.read_loop] .read_end func/rtn .set_scanpath reg/copy P10, P2 func/bcall NULL, [db_set_scanpath], P10 op/incr local/rtn .extract_fields % % Extract all fields from a single data record % The record is in register P1 % Fields will be extracted into registers P2-P10 % % Expects the following registers to be set when called: % P11 - path name of first record (base path) or NULL if this is % the first record % P14 - psTime attribute type % P15 - psInteger attribute type % % Integer types will be read from one byte position too early for % an extra byte, then their first byte will be set to zero % so that they can be successfully imported with attr/import % (where the first byte is the sign which we stripped off when saving) % reg/copy P2, (P1, #0, #256) % path name % % Remove null bytes from end of path name % reg/copy P9, [0] reg/save P9, (#0, #0) reg/xscan P10, P2, P9 reg/cmp P10, NULL ne: reg/save P2, (P10) % % Prepend base path to path name unless this is the first record % reg/jmpeq &[.next_field], P11, NULL reg/xload P9, (P11) opx/add P9, P9, P10, #1 reg/copy P2, P11, [/], P2 .next_field reg/copy P3, (P1, #0xFF, #3) % file mode, 0x100-1 reg/save P3, (#0, #0, #0xff000000) % set sign byte to positive reg/copy P4, (P1, #0x101, #9) % file size in bytes, 0x102-1 reg/save P4, (#0, #0, #0xff000000) % set sign byte to positive reg/copy P5, (P1, #0x109, #5) % owner UID, 0x10A-1 reg/save P5, (#0, #0, #0xff000000) % set sign byte to positive reg/copy P6, (P1, #0x10E, #30) % owner name reg/copy P7, (P1, #0x12B, #5) % group GID, 0x12C-1 reg/save P7, (#0, #0, #0xff000000) % set sign byte to positive reg/copy P8, (P1, #0x130, #30) % group name % % Remove null bytes from end of owner name % reg/copy P9, [0] reg/save P9, (#0, #0) reg/xscan P10, P6, P9 reg/cmp P10, NULL ne: reg/save P6, (P10) % % Remove null bytes from end of group name % reg/xscan P10, P8, P9 reg/cmp P10, NULL ne: reg/save P8, (P10) .last_fields reg/copy P9, (P1, #0x14D, #9) % file modification time, 0x14E-1 reg/save P9, (#0, #0, #0xff000000) % set sign byte to positive reg/copy P10, (P1, #0x156, #32) % SHA256 checksum % % Import integers back into psInteger types % attr/import P3, P15, P3 attr/import P4, P15, P4 attr/import P5, P15, P5 attr/import P7, P15, P7 % % Import time back into psTime type (via psInteger) % attr/import PUSH, P15, P9 attr/def P9, P14, PULL local/rtn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Read all records from DB file and populate objects in memory % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_obj_all_records var/addr P0, [db_handle] attr/addr P0, P0, [psPointer] attr/load P12, [psByteStream], P13, [psString], P14, [psTime], P15, [psInteger] attr/mod P0, [psStreamPosition], #10 % rewind to beginning of DB % (after magic header) error/jmp &[.create_cn], ![.prose.error.sys.NoEntry] tree/del ![.prose.db] % delete previously loaded data .create_cn error/clr obj/commitref PUSH, ![.prose], [db] % re-create data container .obj_loop attr/copy P1, P0, P12 reg/jmpeq &[.obj_end], P1, NULL local/jsr &[.extract_fields] obj/commitref P1, PEEK, P2 % pathname, may include dots % so cannot use var/def for this var/def NULL, P15, P1, [mode], P3 % file mode var/def NULL, P15, P1, [size], P4 % file size in bytes var/def NULL, P15, P1, [uid], P5 % owner UID var/def NULL, P13, P1, [owner], P6 % owner name var/def NULL, P15, P1, [gid], P7 % group GID var/def NULL, P13, P1, [group], P8 % group name var/def NULL, P14, P1, [mtime], P9 % file modification time var/def NULL, P13, P1, [sha], P10 % SHA256 checksum local/jmp &[.obj_loop] .obj_end func/rtn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Set path to scan from existing DB file % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_set_scanpath var/addr P0, [path] attr/copy P2, P0, [psString] reg/copy P3, P2 error/jmp &[.scanpath_missing], ![.prose.error.sys.NoEntry] func/bcall P1, [scan_opendir], P2 error/jmp % % Make sure path is to a directory, not a file % attr/addr P2, P1 attr/index A, P2, [statMode] func/bcall NULL, [scan_closedir], P1 opa/and #040000 reg/jmpne &[.scanpath_nodir], A, #040000 func/rtn .scanpath_missing reg/copy P0, [Path to scan no longer exists: ], P3 error/now ![.prose.error.frecon.BadPath], P0 .scanpath_nodir reg/copy P0, [Path to scan is no longer a directory: ], P3 error/now ![.prose.error.frecon.BadPath], P0 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Create a new database record given the path to a file and % its associated file object and save to the database file % as the given record number (recno) where 0 is the 1st record % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_write_record var/addr P0, [file], P1, [recno], P5, [cksum] attr/load P10, [psPointer] attr/index P5, P5, [psBoolean] attr/addr P0, P0, P10 error/jmp &[.rec_overflow], ![.prose.error.sys.Overflow] class/test P0, [psFileHook] % % Copy psFilePath attribute if this is the top-level file object % Copy psShortPath attribute if this is a subordinate file object % eq: attr/load P2, [psFilePath] ne: attr/load P2, [psShortPath] .copy_path % % Export string to force correct field size % attr/export P2, P0, P2, #256 % pathname field is 256 bytes reg/save P2, (#0x177) % full record size is 330 bytes .copy_mode % % Copy file mode into record % obj/refresh P0 % make sure stat data is up to date attr/export P3, P0, [statMode], #3 % file mode field is 2 bytes so we reg/copy P3, (P3, #1) % need to throw away the sign byte reg/save P2, (P3, #0x100) % file mode offset is +0x100 .copy_size % % Copy file size into record % attr/load P11, [statSize] attr/export P3, P0, P11, #9 % file size field is 8 bytes so we reg/copy P3, (P3, #1) % need to throw away the sign byte reg/save P2, (P3, #0x102) % file size offset is +0x102 .copy_uid % % Copy owner UID into record % attr/export P3, P0, [statUid], #5 % owner UID field is 4 bytes so we reg/copy P3, (P3, #1) % need to throw away the sign byte reg/save P2, (P3, #0x10A) % owner UID offset is +0x10A % % Copy owner name into record % attr/copy P3, P0, [statUidName] reg/save P3, (#30) % owner name field is 30 bytes reg/save P2, (P3, #0x10E) % owner name offset is +0x10E .copy_gid % % Copy group GID into record % attr/export P3, P0, [statGid], #5 % group GID field is 4 bytes so we reg/copy P3, (P3, #1) % need to throw away the sign byte reg/save P2, (P3, #0x12C) % group GID offset is +0x12C % % Copy group name into record % attr/copy P3, P0, [statGidName] reg/save P3, (#30) % group name field is 30 bytes reg/save P2, (P3, #0x130) % group name offset is +0x130 .copy_modtime % % Copy file modification time into record, first normalising for UTC % and removing any subsecond component % attr/load P4, [psTime] var/local P3, P4, [mtime] attr/direct P3, P4, P0, [statLastModify] attr/copy P3, P3, [psTimeSeconds] attr/def P3, [psInteger], P3 attr/export P3, P3, #9 % modtime field is 8 bytes so we reg/copy P3, (P3, #1) % need to throw away the sign byte reg/save P2, (P3, #0x14E) % modtime offset is +0x14E .copy_checksum reg/jmpeq &[.save_record], P5, #0 % skip checksum if not requested % % Calculate checksum and copy into record % func/bcall P3, [sha256], P0 reg/copy P3, P3 reg/save P2, (P3, #0x156) % checksum offset is +0x156 .save_record % % Calculate byte offset position in the database file to store the record % and minimum size that the db file must now be in order to contain % the new record % attr/load P13, [psInteger] opo/mult P1, P1, #0x177 % record size is 0x177 opo/add P1, P1, #10 % add on length of magic header attr/xcopy PUSH, P1, P13 opx/add PEEK, PEEK, #0x177 % % Resize database file if required % var/addr P3, [db_handle] attr/addr P3, P3, P10 obj/refresh P3 % make sure we have latest file size attr/xcopy P4, P3, P11 reg/jmpge &[.write_data], P4, PEEK attr/mod P3, P11, PULL .write_data % % Save the record % attr/direct P3, [psStreamPosition], P1, P13 attr/add P3, [psStreamOut], P2 func/rtn .rec_overflow % % Add context to error message if an overflow has occurred % attr/copy PUSH, P0, [psShortPath] reg/copy PUSH, [Error processing pathname: ], PULL error/now PERR, PULL %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Return number of records in database % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_num_records var/addr P0, [db_handle] attr/addr P0, P0, [psPointer] obj/refresh P0 attr/xcopy P0, P0, [statSize] opx/sub P0, P0, #10 % take off length of magic header opx/div P0, P0, #0x177 % divide by record size func/rtn P0 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Dump contents of database to output stream % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_dump var/addr P0, [db_handle] attr/addr P0, P0, [psPointer] reg/load P9, ![.prose.sys.io.stdout] attr/load P10, [psStreamOut], P12, [psByteStream], P13, [psString], P14, [psTime], P15, [psInteger] attr/mod P0, [psStreamPosition], #10 % rewind to beginning of DB % (after magic header) attr/def P8, P15, #0 .dump_loop attr/copy P1, P0, P12 reg/jmpeq &[.dump_end], P1, NULL op/incr P8 % % Don't have enough registers to do this, so push a few that we don't % immediately need, piece together an output string based on the % extracted fields, then pull the original registers back to % complete the task % stack/push P8, P9, P10 local/jsr &[.extract_fields] attr/index P3, P3 reg/conv P3, P3, #8 func/bcall P10, [sha256_convert], P10 reg/copy P2, [\n ], P2, [\n MODE ], P3, [\n SIZE ], P4, [\n OWNER ], P6, [ (UID ], P5, [)\n GROUP ], P8, [ (GID ], P7, [)\n MTIME ], P9, [\n SHA256 ], P10 stack/pull P10, P9, P8 attr/def P3, P8 attr/mvadd P9, P10, [\n---------------- RECORD #], P3, P2, [\n] local/jmp &[.dump_loop] .dump_end func/call NULL, [get_enter] func/rtn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Truncate database records leaving an empty database file % with just the magic header and nothing else % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .db_truncate var/addr P0, [db_handle] attr/addr P0, P0, [psPointer] attr/mod P0, [statSize], #10 func/rtn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Remove null bytes from end of the string in P0 % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .strip_null reg/copy PUSH, [0] reg/save PEEK, (#0, #0) reg/xscan PUSH, P0, PULL reg/jmpeq &[.strip_rtn], PEEK, NULL reg/save P0, (PULL) .strip_rtn local/rtn