Files
sibo-playground/docs/reference/05-filesystem-dbf.md
T

41 KiB
Raw Blame History

Psion SIBO / Workabout MX — File System and DBF Database API Reference

Source: PLIB Reference, chapters Files (ch. 11) and Database Files (ch. 14), with corroborating structural detail from EPOC O/S System Services, chapter 20 (Database File Management). Everything below is drawn from those manuals; page-anchored citations are given as [PLIB p.NNN] / [SysSvc §20]. OCR-garbled items are flagged explicitly.

OCR caveat. The source is scanned OCR text. Identifiers written p_open, function-name casing, and hex constants have been normalised to their obvious intended form. Where a signature itself is corrupted or truncated in the source, this is called out inline. Do not treat a normalised name as a guarantee of exact header spelling — verify against p_file.h / p_dbf.h before compiling.


Part 1 — The File System

1.1 The file server and file systems (nodes)

  • All file operations are performed by a high-priority system process, the file server (process name SYS$FSRV). A process must connect to it before use; the C startup module normally does this automatically. Sending a message to the file server without connecting panics with panic number 41. [PLIB p.117]
  • The file server supports multiple file systems, also called nodes. p_open can list them. Three were implemented at the time of writing [PLIB p.117]:
    • LOC:: — the local filing system, with the RAM drive M: and SSD drives A:, B:, … (count depends on hardware).
    • REM:: — the remote filing system (present only while connected to a remote file server).
    • ROM:: — the ROM filing system for ROM-based files; normally invisible to the user; does not support devices or directories.
  • Within LOC:: (and REM:: when the remote end is a PC or another SIBO machine), the device and directory structure is MSDOS-compatible. [PLIB p.117]

Media types (SSDs). RAM SSDs (battery-backed static RAM, block-structured, not buffered on SIBO) and Flash SSDs (linked variable-length records). Overwriting or deleting on Flash consumes space that is only reclaimed by reformatting; a byte can be physically overwritten with no storage penalty only if the new value is derived from the old by clearing bits to zero. This is the property the DBF layer exploits. [PLIB pp.118120, 195]

1.2 File specification structure

A full file specification has the form [PLIB p.120]:

<node><device><dir><name><ext>

Example: LOC::B:\NOTES\OLD\PLANS.TPD, where:

Component Meaning Example
<node> file system node LOC::
<device> device name B:
<dir> directory name \NOTES\OLD\
<name> file name PLANS
<ext> extension name .TPD

Rules [PLIB p.120]:

  • A file specification never exceeds P_FNAMESIZE (128) bytes including the zero terminator.
  • The <node> component is always P_FSYSNAMESIZE bytes long (excluding any zero terminator).
  • Beyond <node> and the P_FNAMESIZE total, make no assumptions about component sizes; the syntax of <device>/<dir>/<name>/<ext> is owned by the node's file-system code (foreign remote systems can map onto this model, e.g. VMS or Mac paths). Use p_fparse/p_chdir rather than manipulating specs by hand.

Default path. The file server stores a per-client default path (<node><device><dir>) plus a system-wide default assigned to new clients. Manipulate with: p_setpth / p_setpthasync (set this process's default), p_getpth (get it), p_getpthbyid (get another process's), p_setdefaultpath (set the system-wide default). [PLIB pp.121, 126]

Parsing / directory manipulation.

  • INT p_fparse(TEXT *name, TEXT *related, TEXT *full, P_FPARSE *perk); — builds a full spec into full (reserve P_FNAMESIZE bytes; P_FNAMESIZE bytes are always written). Components are taken from name, then related (may be NULL), then the default path, in that order of precedence. Output is upper-cased. perk (may be NULL) receives a P_FPARSE struct with the lengths of each component and a wildcard-flags byte (P_PWILD_ANY, P_PWILD_NAME, P_PWILD_EXT). f_fparse is identical but calls p_leave on error instead of returning it; p_fparseasync adds a trailing WORD *stat. [PLIB pp.123124]
  • INT p_chdir(TEXT *src, TEXT *outp, INT mode, TEXT *subdir); — parse src and change its directory per mode: P_CD_ROOT, P_CD_PARENT, or P_CD_SUBDIR (append zero-terminated subdir). Reserve P_FNAMESIZE at outp. Async form p_chdirasync. [PLIB p.125]

1.3 p_open — modes, formats, and access flags

Channel-based file services all go through p_open:

INT p_open(VOID **ppfcb, TEXT *name, UINT mode);

On success returns 0 and writes the channel handle to *ppfcb (not written on failure). name is parsed with a NULL related name. [PLIB p.142]

mode is a bitwise-OR of exactly one open mode, exactly one format, and any combination of access flags.

Open modes (choose exactly one) [PLIB pp.142143]

Mode Behaviour
P_FOPEN Open an existing file. E_FILE_NXIST if it does not exist. Normally used for read access.
P_FCREATE Create a file that must not already exist. E_FILE_EXIST if it does. Requires P_FUPDATE for write.
P_FREPLACE If the file exists, open and truncate to zero length; otherwise create it. Requires P_FUPDATE for write.
P_FAPPEND Same as P_FOPEN but initial position is at end of file so the next write appends. P_FRANDOM not needed; P_FUPDATE needed for write.
P_FUNIQUE Create a unique file, using the passed path as the related path; the unique name is written back to name (reserve P_FNAMESIZE bytes). P_FUPDATE not needed.

Formats (choose exactly one) [PLIB pp.142, 146147; §list p.121]

Format Meaning
P_FSTREAM Flat binary file.
P_FSTREAM_TEXT Flat binary file, but declares the file is text; on REM:: it makes the remote side present/parse CRLF-terminated records. Locally usually identical to P_FSTREAM; no penalty, potential gain on remote. Prefer this for self-processed text files.
P_FTEXT Record-oriented text file. Implemented as a client-side layer (the TXT: device) over P_FSTREAM_TEXT; data is buffered in that layer, so flushing is needed.
P_FDIR Open a directory-listing channel (list files/subdirectories).
P_FDEVICE Open a device-listing channel (list devices of a node).
P_FNODE Open a node-listing channel (list file systems).
P_FFORMAT Open a device-format channel (LOC:: only at time of writing; OR in P_FLOWDENSITY for dual-density low-density format).

Access flags (combine as needed) [PLIB p.143]

Flag Meaning
P_FUPDATE Write access as well as read. Writing without it → E_FILE_RDONLY.
P_FRANDOM Random access required; needed to use p_seek. Without it, p_seekE_FILE_INV. Do not specify unless you will seek (the FS may optimise sequential-only access).
P_FSHARE Do not block the file from being re-opened for read access. Without it, a later open → E_FILE_LOCKED. Cannot be combined with P_FUPDATE (shared write is unsupported).

Sharing rules: any number of processes may open the same file for reading only, provided all readers specify P_FSHARE. Multiple writers are never allowed. Once open for reading it may be re-opened for reading but not writing; once open for writing it may not be re-opened at all. [PLIB p.140]

Examples [PLIB p.140]:

p_open(&fcb, "fred.dat", P_FSTREAM | P_FSHARE);                          /* read only  */
p_open(&fcb, "fred.dat", P_FSTREAM | P_FUPDATE | P_FREPLACE | P_FRANDOM);/* writable   */

Selected p_open errors: E_GEN_NOMEMORY, E_GEN_ARG (illegal flag combination), E_FILE_DEVICE, E_FILE_NOTREADY, E_FILE_EXIST, E_FILE_NXIST, E_FILE_ACCESS, E_FILE_DIRFULL, E_FILE_PROTECT, E_FILE_FULL, E_FILE_LOCKED, E_FILE_DIR, plus any p_fparse error. [PLIB pp.143144]

1.4 Reading and writing streams

INT p_read (VOID *pfcb, VOID *buf, UINT len);
INT p_write(VOID *pfcb, VOID *buf, UINT len);
INT p_close(VOID *pfcb);
  • p_read reads len bytes (or the bytes remaining before EOF, whichever is smaller) from the current position; returns the number of bytes read. At EOF, reads 0 bytes and returns E_FILE_EOF. len must not exceed P_FMAXSSIZE (16K). Most efficient in multiples of P_FBLKSIZE (512) on 512-byte boundaries. Other errors: E_FILE_ABORT, E_FILE_READ. [PLIB p.144]
  • p_write writes len bytes at the current position; increments position by bytes written; returns 0 on success. lenP_FMAXSSIZE. On Flash you may overwrite a single byte (len==1) if the new byte only clears bits (then the modification date is not changed). Errors: E_FILE_FULL, E_FILE_RDONLY, E_FILE_ABORT, E_FILE_WRITE. [PLIB p.144]
  • p_close(NULL) is harmless (returns 0). Close may perform a final buffered write and a date update, so it can return p_write/p_fdate errors — but it always closes the channel. To handle failures cleanly, p_iow(pfcb, P_FFLUSH) first. LOC:: on SIBO does not buffer written data; EPOC-on-PC does. [PLIB p.143]

p_iow control operations on a binary/text channel [PLIB pp.145146]:

  • INT p_iow(VOID *pfcb, P_FFLUSH); — flush buffered data and write the modification date.
  • INT p_iow(VOID *pfcb, P_FSETEOF, ULONG *peof); — set the logical EOF to *peof (requires P_FUPDATE). Extending pre-allocates storage on block devices; truncating also reduces the current position if needed.
  • INT p_iow(VOID *pfcb, P_FCANCEL); — cancel pending async requests (not actively supported; a no-op even when a request is pending — simulate a cancel instead).

1.5 Seeking

Binary / stream:

INT p_seek(VOID *pfcb, INT sense, LONG *ppos);

sense is one of [PLIB p.144]:

sense Meaning
P_FABS set position to *ppos
P_FEND set position to *ppos relative to end-of-file
P_FCUR set position to *ppos relative to current position

On success the new position is written back to *ppos and 0 returned. E_FILE_INV if the channel was not opened with P_FRANDOM. A negative resulting position clamps to the start; beyond EOF clamps to EOF (use P_FSETEOF to extend). Setting *ppos=0 with P_FCUR senses the current position; with P_FEND senses the file length. f_seek is the p_leave-on-error variant. On P_FSTREAM_TEXT remote channels, only "seek to 0 (P_FABS)" and "seek to end (P_FEND, rel 0)" are guaranteed. [PLIB pp.144145]

Record-oriented text (P_FTEXT): p_seek sets the read position only (writes always go to EOF), and sense is different [PLIB pp.149]:

sense Meaning
P_FREWIND position to the first record (ppos ignored)
P_FRSENSE get the position of the last record read/written
P_FRSET set the record position previously got with P_FRSENSE

Requires P_FRANDOM (else E_FILE_INV). Some remote systems may not support P_FRSET/P_FRSENSE.

1.6 Text records (P_FTEXT)

Convention: records are terminated by CRLF (CR = 13, LF = 10); a file may optionally end with SUB (26). On read, the parser also accepts CR, LF, or LFCR as a terminator and treats a SUB as EOF; on write it appends CRLF only (no SUB). Record content must not exceed P_FMAXRSIZE (256) bytes and must not contain CR, LF, or SUB. [PLIB pp.146148]

  • p_read returns the record length (bytes written to buf), positioning to the next record. If len < record length, the first len bytes are read and E_FILE_RECORD returned (still advances). Returns 0 on a zero-length record; E_FILE_EOF past the last record. [PLIB p.148]
  • p_write writes a record of len bytes (0 … P_FMAXRSIZE); always appended at EOF; must not contain delimiters. Errors incl. E_FILE_RECORD if oversize. [PLIB p.149]

1.7 Directory, device, and node listing

All three use the open → repeated p_iow(..., P_FREAD, ...) until E_FILE_EOF → close pattern.

  • Nodes: p_open(&ncb, "FIL:" or NULL, P_FNODE); each p_iow(ncb, P_FREAD, buf, pinfo) writes the next node name (buf capacity P_FSYSNAMESIZE+1, i.e. 6). Optional pinfo (P_NINFO*) gets the same info as p_ninfo. [PLIB p.127]
  • Devices: p_open(&dcb, node-name, P_FDEVICE); E_GEN_FSYS if the node is bad, E_GEN_NSUP if the node has no devices (e.g. ROM::). Each p_iow(P_FREAD) writes the next device name (buf capacity P_FNAMESIZE); trailing arg NULL. [PLIB p.128]
  • Files: p_open(&dcb, name, P_FDIR); name is parsed with a wildcard related name (*.*), so a name of "" lists the current directory. Each p_iow(P_FREAD, buf, pinfo) writes the next matching file name (excluding node/device/dir; buf capacity P_FNAMESIZE). Optional pinfo (P_INFO*) carries per-file info. A root directory of a PC-based device may return a volume-name entry with P_FAVOLUME set. [PLIB pp.133134]

P_INFO (from p_file.h) [PLIB p.134]:

typedef struct {
    UWORD version;
    UWORD status;   /* status bits */
    ULONG size;     /* size of the file in bytes (end-of-file position) */
    ULONG modst;    /* system time of last modification (secs since 1970-01-01) */
    UBYTE spare[4];
} P_INFO;

status bit fields: P_FAWRITE (not read-only), P_FAMOD (modified), P_FAHIDDEN, P_FASYSTEM, P_FADIR (directory file), P_FAVOLUME (volume-name directory), P_FATEXT (recognised text file; LOC:: cannot recognise text files). [PLIB p.134]

1.8 File / device / node information and non-channel operations

  • INT p_finfo(TEXT *name, P_INFO *pinfo); — info on one file/directory (same fields as a P_FDIR read). Good atomic existence check (E_FILE_NXIST if absent). Async p_finfoasync. [PLIB p.135]
  • INT p_testpth(TEXT *dname); — returns 0 if the directory component exists. [PLIB p.135]
  • INT p_ninfo(TEXT *node, P_NINFO *pninfo); — node info. P_NINFO { UWORD version; UWORD type; UWORD formattable; UBYTE spare[26]; }. type is P_FSYSTYPE_FLAT or P_FSYSTYPE_HIER. [PLIB pp.127128]
  • INT p_dinfo(TEXT *dname, P_DINFO *pdinfo); — device + mounted-medium info. P_DINFO { UWORD version; UWORD mediatype; UWORD removable; ULONG size; ULONG free; UBYTE name[P_VOLUMENAME]; WORD batterystate; UBYTE spare[16]; }. Low byte of mediatype: P_FMEDIA_UNKNOWN|FLOPPY|HARDDISK|RAM|FLASH|ROM|WRITEPROTECTED; high byte flags incl. P_FMEDIA_COMPRESSIBLE (worth compressing out deleted records — not set for Flash), P_FMEDIA_DYNAMIC, P_FMEDIA_INTERNAL, P_FMEDIA_DUAL_DENSITY, P_FMEDIA_FORMATTABLE. [PLIB pp.129130]
  • Other non-channel calls (each with an …async twin): p_rename, p_delete (directory must be empty), p_mkdir (creates intermediate dirs), p_sfstat (set attributes; with P_FAVOLUME set in the mask, sets/deletes the volume label), p_fdate (set modification date, ≥ 1980-01-01). [PLIB pp.136139]
  • Formatting: open with P_FFORMAT then repeatedly p_read (first read yields a UWORD total count, subsequent reads step the format, E_FILE_EOF when done). Aborting early corrupts the medium. [PLIB pp.131132]

Part 2 — The DBF Database API

2.1 The model

A database file (DBF) is a binary file of typed, variable-length records. Used by MC Diary, Series 3 Database, and OPL data files. DBFs are Flash-friendly: records can be appended, deleted, or replaced in place on a Flash SSD without rewriting the whole file. [PLIB p.195]

Key model properties [PLIB pp.195197, 204207]:

  • Record type 0 = deleted. Deleting a record only overwrites its 4-bit type field with zero (which merely clears bits — legal on Flash). Therefore deleting does not shrink the file.
  • Append-only; updates = erase + append. Updating a record deletes the original and appends the modified version, so an update always moves the record to the end of the file.
  • Reclaiming space: DbfCompress (only on a compressible medium — not Flash) or copy the file record-by-record with DbfCopyFile (deleted records are not copied).
  • Sparse index (optional): one 4-byte address per sixteenth record, held in a separate segment (DBF$nnnn.INX, nnnn a 4-hex-digit number from the channel) so it does not consume the app's data space. Enables fast random access and fast backward scans; adjusted on append/delete so it always points to every 16th record.
  • Read-ahead buffer: each file-server read fills a caller-supplied buffer (typically 4K), usually pulling in many records; a read for a record already in the buffer just locates it. Most DBF services may overwrite the buffer. The next read after the buffer is disturbed re-reads the whole buffer (a performance hit), so any direct modification of the buffer that is not done via a DBF service must be followed by DbfTrash. Services guaranteed not to alter the buffer: DbfFlush, DbfVersion, DbfAppend, DbfSense, DbfCount. [PLIB p.196]

Record limit: max 65534 records of any one visible type, numbered 0…65533. Since only one type is visible per open, a file may hold more in aggregate. A file with more than the max (of the visible type) is logically truncated to the max on open. [PLIB p.197]

End-of-file record. Reading past the last record returns E_FILE_EOF; the current record number (per DbfSense) then becomes last record + 1 — the fictitious end-of-file record. Reading before the first record (DbfBackRead/DbfFindRead backwards) also gives E_FILE_EOF with current record number 0. If the file has no records, DbfSense always returns 0. Services that operate on the current record (DbfEraseRead, DbfUpdate, …) do nothing and return E_FILE_EOF when positioned on the EOF record. [PLIB p.197]

2.2 The file header

DBFs begin with a 22-byte standard header [PLIB p.196; SysSvc §20]:

Byte offset Contents
015 Zero-terminated file signature (all 16 bytes are verified — pad with trailing zeros).
16, 17 Version of DBF software used to produce the file.
18, 19 Offset from start of file to the first record.
20, 21 Minimum version of DBF software required.

The first-record offset allows an extended header (application-specific data after the standard header). If none, the value is 22. The first record is always the type-2 field information record. Version-number format: see DbfVersion. [PLIB p.196; SysSvc §20]

2.3 Records and the record header word

In memory, a record is a DbfRecord (p_dbf.h) [PLIB p.196]:

typedef struct {
    UWORD header;    /* record header word */
    UBYTE data[2];   /* data to be written... (variable length in practice) */
} DbfRecord;

The header word: top 4 bits = record type (015); low 12 bits = record length. Maximum record length is 4094 bytes (one less than the theoretical 0xFFF = 4095, so a 4094-byte record plus its 2-byte header fits a 4096-byte buffer). [PLIB p.196; SysSvc §20]

Record types [PLIB pp.196197; SysSvc §20]:

Type Meaning
0 Deleted record — ignored by all DBF services; never copied by DbfCopyFile.
1 Standard data record (fields per the field information record). Usually the only visible type.
2 Field information record (FIR) — must exist and be the first record; later type-2 records ignored.
3 Descriptive record — optional, file-wide app data (see DbfDescRecordRead/Write).
47 App-specific: copied to a new file, not appended to an existing file, by DbfCopyFile.
813 App-specific: both copied to a new file and appended to an existing file (merged) by DbfCopyFile.
14 Reserved for voice records.
15 Reserved for internal use — do not use.

2.4 The Field Information Record (FIR) and field types

The FIR (type 2) holds up to 32 bytes, one per field, giving each field's type [PLIB p.197; SysSvc §20]:

Byte value Field type
0 Word
1 Long
2 Double
3 String
4255 Reserved

So a data record has at most 32 fields — with one exception: a record that contains only string fields is not bound by the 32-field limit and may hold any number of fields, subject to the 4094-byte record cap. Records may contain fewer fields than the FIR lists, provided only trailing fields are omitted; records with more fields than the FIR are assumed to have the extra ones as string fields. Only DbfFindRead/DbfFindReadField assume records match the FIR. [PLIB p.197]

2.5 String fields (leading byte count) and continuation sub-fields

A string field is leading byte-counted text, so a normal string field holds at most 255 characters. Longer strings use continuation sub-fields: the first 254 characters plus a terminating byte 0x14, stored in a string field whose count byte is 255; the 0x14 + count-255 signals that the immediately following string field continues the text. This chains indefinitely, subject only to the 4094-byte record limit. [PLIB p.197]

2.6 The structs

DbfHeader (p_dbf.h) — passed to open [PLIB p.199]:

typedef struct {
    UBYTE fileType[DbfHeaderNameSize]; /* 16-byte file signature */
    UWORD createVersion;               /* software version used to create the file */
    UWORD dataStart;                   /* offset in file of first record (22 if no ext header) */
    UWORD needVersion;                 /* minimum software version needed to handle this file */
    UWORD firHeader;                   /* header word for the field information record */
    UBYTE fir[DbfMaxFirLength];        /* the field information record bytes */
} DbfHeader;
  • When creating/replacing, pre-fill all elements (header + FIR). There is no gap between the header and FIR in the struct even when an extended header is used; the file leaves a gap of dataStart 22 bytes for it.
  • When opening existing, pre-fill only fileType (the signature); the rest is filled from the file. All 16 signature bytes are verified — mismatch → E_FILE_INVALID.
  • DbfHeaderNameSize = 16 (implied by "16-byte file signature"). DbfMaxFirLength is the FIR capacity; the FIR maximum length is 32. (Exact numeric value of DbfMaxFirLength not stated verbatim in the source beyond the max-32 rule — verify in p_dbf.h.)

DbfOpenArgs (p_dbf.h) — for DbfQuickOpen [PLIB p.200]:

typedef struct {
    VOID       **pFcb;
    UBYTE       *fName;
    UINT         mode;
    DbfHeader   *pHead;
} DbfOpenArgs;

DbfRecord — see §2.3.

2.7 The DBF functions

Every DBF function calls p_panic if pFcb is not a valid DBF channel from DbfOpen/ DbfQuickOpen; that clause is omitted per-entry below. Unless noted "may be used on a DBF opened without an index", the without-index behaviour is called out where the manual specifies it.

Opening / closing / flushing

  • INT DbfOpen(INT *pstate, VOID **pFcb, TEXT *fName, UINT mode, DbfHeader *pHead, UBYTE *pbuffer, UINT len, UINT type); Open a DBF channel. mode = exactly one of P_FOPEN/P_FCREATE/P_FREPLACE/P_FAPPEND/ P_FUNIQUE, optionally OR'd with P_FUPDATE and/or P_FSHARE (other required stream flags are supplied automatically; P_FOPEN and P_FAPPEND are treated identically). pbuffer/len is the caller's read-ahead buffer: len must be 512…16384 (else E_FILE_RECORD), and must be at least as large as the largest record (4096 is guaranteed sufficient). Only records of type type are visible (normally 1; may be 414; results undefined for 0/2/3/>14). *pstate selects the open strategy [PLIB pp.198199]:

    • DbfStateDisabled — open with a full sparse index; returns only when the index is fully built (may take a while).
    • DbfStateOpenNoIndex — open without an index; returns fast, but some services are then disallowed (see each function).
    • DbfStateStart — open with a sparse index incrementally: call DbfOpen repeatedly, feeding back *pstate each time, until *pstate becomes DbfStateStart again (as printed — the source states the loop terminates when the written-back value is DbfStateStart; this reads like an OCR/spec inconsistency, likely intended to be a distinct "finished" state such as DbfStateDisabled/DbfStateEnd. Flag: verify the terminating state in p_dbf.h.).

    After a successful open the current record is 0, so DbfNextRead reads record 1 and DbfEraseRead erases record 0; use DbfFirstRead to read record 0. Errors: those of p_open(P_FSTREAM), p_seek, p_read, plus E_FILE_INVALID (bad signature / bad FIR), E_FILE_RECORD (bad buffer length). [PLIB p.199]

  • INT DbfQuickOpen(INT *pstate, DbfOpenArgs *pargs, UBYTE *pbuffer, UINT len, UINT type); As DbfOpen but pFcb/fName/mode/pHead are bundled in DbfOpenArgs. Preferred over DbfOpen (more efficient, shorter code); DbfOpen retained for compatibility. [PLIB p.200]

  • INT DbfClose(VOID *pFcb); — close the file (returns as p_close); channel is closed even on error. May be used without an index. [PLIB p.200]

  • INT DbfFlush(VOID *pFcb); — flush all buffers so modified data is written (returns as p_write). Does not alter the buffer. May be used without an index. [PLIB p.200]

  • VOID DbfTrash(VOID *pFcb); — tell the DBF layer the read-ahead buffer was overwritten by the caller and can no longer be relied on. May be used without an index. [PLIB p.200]

  • INT DbfCopyDown(VOID *pFcb, UINT offset); — copy the record at offset in the buffer to the start of the buffer (and flag the buffer invalid, so no DbfTrash needed); returns the record length. offset must be one previously returned by a read service and the caller must not have written to the buffer since. May be used without an index. [PLIB p.201]

Whole-file operations

  • INT DbfCompress(UINT *pstate, VOID *pFcb); — reclaim space from deleted records if the medium is compressible (otherwise a no-op that still returns 0). After a real compress the current record is the EOF record. *pstate = DbfStateDisabled (blocking) or DbfStateStart (incremental loop, terminating as written back). Should not be used without an index. [PLIB p.201]

  • INT DbfCopyFile(UINT *pstate, VOID *pFcb, TEXT *pTargetName, UINT targetMode, UINT type, INT dir); Copy non-deleted records between the current file and pTargetName. targetMode = same options as open (target always opened without an index; P_FUNIQUE writes back the unique name). type selects a single type (usually 1) or DbfRecordTypeAll for all types. dir is DbfCopyFromHandle (current → target; target new file with P_FCREATE/P_FREPLACE/P_FUNIQUE, or append with P_FOPEN/P_FAPPEND) or DbfCopyToHandle (target → current; target sensibly P_FOPEN). Copying to a new file always copies the FIR (type 2) and the file header (incl. extended header) regardless of type; appending never copies types 27. Appending requires matching signatures and compatible FIRs (identical, or both string-only) else E_FILE_INVALID. *pstate supports DbfStateDisabled, DbfStateStart (est. calls = file size / buffer size + 2), and DbfStateCopyAbort (abort an in-progress DbfStateStart copy). May be used without an index. Warning: appending can exceed 65534 records with no error. [PLIB pp.201202]

  • INT DbfFileSize(VOID *pFcb, ULONG *pSize); — write the open file's size to *pSize. May be used without an index. [PLIB p.202]

  • UINT DbfVersion(VOID); — DBF software version as hex xyyF: x = major (4 bits), yy = minor (8 bits), F = release type A/B/F (Alpha/Beta/Final, 4 bits). E.g. 0x110F → 1.10F. Only the major number gates whether a file can be handled. May be used without an index. [PLIB p.203]

Extended header and descriptive record

  • INT DbfExtHeaderRead(UINT cont, VOID *pFcb, VOID *buf, UINT len); — read up to len bytes of the extended header; returns bytes read. cont: 0 = initial read (resets to start of ext header); 1 = continue; use 0 if reading the whole thing in one call. E_FILE_EOF at end. May be used without an index. [PLIB p.202]
  • INT DbfExtHeaderWrite(UINT cont, VOID *pFcb, VOID *buf, UINT len); — symmetric writer. May be used without an index. [PLIB pp.202203]
  • INT DbfDescRecordRead(VOID *pFcb); — read the descriptive record to offset 0 of the read-ahead buffer; returns its length, or E_FILE_EOF if none, or E_FILE_INVALID if the file was opened without an index. Sub-records use the same word-header format as main records; ignore (do not delete) unrecognised sub-record types. [PLIB p.203]
  • INT DbfDescRecordWrite(VOID *pFcb, UINT len); — write a descriptive record from a DbfRecord at buffer offset 0 (content starts at offset 2; the leading 2 bytes form the header and are not counted in len). Any existing descriptive record is erased first (max one per file); len==0 just erases it. E_FILE_INVALID if opened without an index. [PLIB p.203]

Reading records

All read services return the record data length (excluding the 2-byte header) and write the buffer offset of the DbfRecord (including its header) to *pOffset. The read record becomes the current record. On E_FILE_EOF, *pOffset is invalid.

  • INT DbfAbsRead(VOID *pFcb, UINT recnum, UWORD *pOffset); — seek to and read record recnum. E_FILE_EOF if recnum > last record (current → EOF record). May be used without an index. [PLIB p.204]
  • INT DbfAbsReadSense(VOID *pFcb, UINT recnum, UWORD *pOffset, ULONG *pPos); — as DbfAbsRead but also writes the file position of the record header to *pPos. May be used without an index. [PLIB p.204]
  • INT DbfNextRead(VOID *pFcb, UWORD *pOffset); — read the next record. E_FILE_EOF if already on the last record / no records (current → EOF record). May be used without an index. [PLIB p.204]
  • INT DbfBackRead(VOID *pFcb, UWORD *pOffset); — read the previous record. E_FILE_EOF if on the first record / no records (current → record 0). May be used without an index. [PLIB p.204]
  • INT DbfFirstRead(VOID *pFcb, UWORD *pOffset); — read the first record. E_FILE_EOF if no records of the current type (current → 0 = EOF record). May be used without an index. [PLIB p.205]
  • INT DbfLastRead(VOID *pFcb, UWORD *pOffset); — read the last record. E_FILE_EOF if none. Should not be used without an index. [PLIB p.205]

Appending / erasing / updating

  • INT DbfAppend(VOID *pFcb, UINT len); — append a record of the current type and length len (data only) to EOF and make it current; returns 0. The record must be at the start of the read-ahead buffer as a DbfRecord including its 2-byte header; DbfAppend builds the type/length header from those two bytes (not counted in len). E_GEN_OVER if already 65534 records of the type; E_FILE_RECORD if total (data + 2) exceeds the buffer. Does not alter the buffer. Should not be used without an index. [PLIB p.205]
  • INT DbfEraseRead(INT *pstate, VOID *pFcb, UWORD *pOffset); — erase the current record and read the following one (becomes current); returns its length or negative. Two E_FILE_EOF cases: (a) already on the EOF record / no records → does nothing; (b) current was the last record → it is erased and current becomes the EOF record. Distinguish via DbfSense+DbfCount, or by comparing DbfCount before/after. *pstate = DbfStateDisabled / DbfStateStart (incremental). Should not be used without an index. [PLIB p.206]
  • INT DbfUpdate(INT *pstate, VOID *pFcb, UINT len); — erase the current record and append a new one of length len from the read-ahead buffer, making it current. The new record must be a DbfRecord at the start of the buffer (leading 2 bytes = header, not counted in len). The old record is not erased until the new one is successfully appended. E_FILE_EOF (no-op) if no records of the current type or if on the EOF record. *pstate = DbfStateDisabled / DbfStateStart. Should not be used without an index. [PLIB p.206] (Note: the manual's prose here says "DbfAppend uses these two bytes" — clearly referring to the same header-construction mechanism.)

Sensing / counting

  • UINT DbfSense(VOID *pFcb); — return the current record number (the EOF record number — 0 if empty, else count+1 — if a preceding call returned E_FILE_EOF). Does not alter the buffer. May be used without an index. [PLIB p.209]
  • UINT DbfCount(VOID *pFcb); — return the number of records of the currently visible type without changing the current record. Does not alter the buffer. Should not be used without an index. [PLIB p.209]

Finding by content

  • INT DbfFindRead(UINT *pstate, VOID *pFcb, VOID *pBuffer, UINT len, UINT findMode, UINT nStrings, UWORD *pOffset); Match wildcard text at pBuffer of length len (≤ 255) against the first nStrings string fields of each record, starting at the current record; returns the matching record's data length or a negative error. Equivalent to DbfFindReadField with startStr = 0. nStrings == DbfFindAllStrings searches all string fields to end-of-record. The FIR is used to type the first 32 fields; fields beyond that are assumed strings. On match, that record becomes current and its buffer offset is written to *pOffset; on no match, E_FILE_EOF and current becomes the first record (backward search) or the EOF record (forward search). May be used without an index except DbfFindLast (unpredictable). Panics if findMode is malformed. [PLIB pp.208209]

  • DbfFindReadField(...) — same as DbfFindRead but with an extra startStr argument: match starts at string field number startStr (0 = first string field). Available only in EPOC ≥ 3.18. OCR FLAG: the signature is garbled/truncated in the source. The printed fragment reads: … UINT nStrings, UWORD *pOffset, UINT startStr); (line lacks the return type, pstate, pFcb, pBuffer, len, findMode). By analogy with DbfFindRead the full signature is almost certainly:

    /* RECONSTRUCTED — verify against p_dbf.h before use */
    INT DbfFindReadField(UINT *pstate, VOID *pFcb, VOID *pBuffer, UINT len,
                         UINT findMode, UINT nStrings, UWORD *pOffset, UINT startStr);
    

    [PLIB pp.207208]

findMode is an OR of three parts [PLIB pp.207208]:

  1. Max match length in any one string field (0…255; 255 = no truncation) occupies the low part of findMode.
  2. Direction / start: DbfFindForwards, DbfFindBackwards, DbfFindFirst, DbfFindLast.
  3. Case: DbfFindCaseIndependent or DbfFindCaseDependent.

Finding across continuation sub-fields (only needed if strings may exceed 255 chars): OR 0x1400 into len (whose base value cannot exceed 255); set the length component of findMode to 255 and OR in 0x4000; only case-independent matching is allowed (OR DbfFindCaseIndependent); then OR in the direction flag as usual. [PLIB p.208]

No per-field getter/setter exists. The DBF API deals in whole records only. There is no function to read or write an individual Word/Long/Double/String field. The application must pack and unpack field bytes by hand into/out of the DbfRecord.data area, following the FIR field order and the string leading-byte-count convention. The find services are the only ones that interpret field structure, and only for string matching.

2.8 Error names (DBF)

E_FILE_EOF (past end / before start), E_FILE_INVALID (bad signature / FIR / no-index misuse), E_FILE_RECORD (bad buffer length or record too big for buffer), E_GEN_OVER (65534-record limit hit on append). DbfClose/DbfFlush propagate p_close/p_write errors; most read services propagate p_seek/p_read errors. [PLIB pp.199, 205206] The System Services manual names the same conditions EofErr, InvalidFileErr, and the panics PanicDbf1 (bad handle) / PanicDbf2 (bad offset). [SysSvc §20]


Part 3 — Worked recipe: create schema → append → read back

Assembled strictly from the manual's prose (PLIB ch. 14). Field packing is done by hand because there is no per-field API. Illustrative — variable names/details are the author's; the API calls, struct layout, and offsets are per the manual.

Schema for the example: two fields — field 0 = Long (FIR byte 1), field 1 = String (FIR byte 3).

Step 1 — Create the file with header + FIR

Pre-fill a DbfHeader completely (creating/replacing requires the full header and FIR; p.199). The FIR is itself a type-2 record, so firHeader is a record header word: top 4 bits = type 2, low 12 bits = FIR length (here 2). Provide a read-ahead buffer of 4096 (guaranteed sufficient; p.199) and open with visible type = 1.

#include <p_dbf.h>

UBYTE   buf[4096];          /* read-ahead buffer, 512..16384; >= largest record */
VOID   *fcb;
INT     state;
DbfHeader h;
DbfOpenArgs args;

/* --- header --- */
p_bfill(&h.fileType[0], DbfHeaderNameSize, 0);      /* zero the 16-byte signature   */
p_scpy(&h.fileType[0], "MYAPPDatabase");            /* pad remainder stays 0 (p.196) */
h.createVersion = DbfVersion();                     /* p.203                         */
h.dataStart     = 22;                               /* no extended header (p.196)    */
h.needVersion   = DbfVersion();
/* --- FIR (type 2, length 2): field0=Long(1), field1=String(3) --- */
h.firHeader = (2 << 12) | 2;                         /* top 4 bits type, low 12 len   */
h.fir[0] = 1;                                        /* Long   (p.197)                */
h.fir[1] = 3;                                        /* String (p.197)                */

args.pFcb  = &fcb;
args.fName = (UBYTE *)"MYDATA.DBF";
args.mode  = P_FCREATE | P_FUPDATE;                 /* create new, writable (Files ch)*/
args.pHead = &h;

state = DbfStateDisabled;                            /* build index, blocking (p.199) */
if (DbfQuickOpen(&state, &args, &buf[0], sizeof(buf), 1) < 0)
    /* handle error (p_fparse / p_open / E_FILE_* errors) */;

Step 2 — Pack a record by hand and append it

A record to be appended must sit at the start of the buffer as a DbfRecord: 2-byte header then data; DbfAppend builds the header from those two bytes and len is the data length only (p.205). Pack the Long field (its byte layout is the app's responsibility), then the String field as a leading count byte followed by its characters (p.197).

DbfRecord *rec = (DbfRecord *)&buf[0];
UBYTE     *d   = &rec->data[0];   /* data starts at buffer offset 2 */
UINT       len = 0;
LONG       idNum = 42;
TEXT      *nameStr = "Widget";
UINT       nameLen = p_slen(nameStr);       /* <= 255 for a plain string field */

/* field 0: Long (4 bytes, app-defined byte order) */
p_bcpy(&d[len], &idNum, sizeof(LONG));  len += sizeof(LONG);
/* field 1: String = leading byte count + chars (p.197) */
d[len++] = (UBYTE)nameLen;
p_bcpy(&d[len], nameStr, nameLen);      len += nameLen;

/* header bytes are set by DbfAppend from rec->header; type is the current type (1) */
if (DbfAppend(fcb, len) < 0)            /* appends, becomes current record (p.205) */
    /* handle E_GEN_OVER / E_FILE_RECORD / p_write errors */;

Step 3 — Read the record back and unpack

After open the current record is 0 (p.199). Reads return the data length and write the DbfRecord offset (header included) into *pOffset. Read the first record, then unpack in FIR order. (DbfCopyDown first if you intend to edit in the buffer — p.201/SysSvc §20.)

UWORD off;
INT   rlen = DbfFirstRead(fcb, &off);   /* read record 0 (p.205) */
if (rlen >= 0) {
    DbfRecord *r = (DbfRecord *)&buf[off];
    UBYTE     *p = &r->data[0];
    LONG       gotId;
    UBYTE      slen;
    TEXT       gotName[256];

    p_bcpy(&gotId, &p[0], sizeof(LONG));   /* field 0: Long   */
    p += sizeof(LONG);
    slen = *p++;                           /* field 1: count byte, then chars (p.197) */
    p_bcpy(&gotName[0], p, slen);
    gotName[slen] = 0;
    /* gotId == 42, gotName == "Widget" */
}

DbfFlush(fcb);      /* ensure written data is on the medium (p.200) */
DbfClose(fcb);      /* p.200 */

Notes tied to the model:

  • To update the record, pack a new DbfRecord at buffer offset 0 and call DbfUpdate(&state, fcb, len); the record is deleted and re-appended at EOF (p.206).
  • To delete, position on it (e.g. DbfAbsRead) and call DbfEraseRead(&state, fcb, &off) (p.206); the file does not shrink until DbfCompress (non-Flash) or a DbfCopyFile rebuild (p.195).
  • DbfFindRead / DbfFindReadField can locate a record by matching a wildcard against its string fields (p.207209); numeric fields are not searchable by these services.