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