Frecon code: db.pal

From PROSE Programming Language - Wiki
Jump to: navigation, search
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% 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