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

726 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_seek``E_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]:
```c
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. `len``P_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]:
```c
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]:
```c
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]:
```c
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]:
```c
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:
```c
/* 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`.
```c
#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`).
```c
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`.)
```c
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.207`209); numeric fields are not searchable by these services.