Files

726 lines
41 KiB
Markdown
Raw Permalink Normal View 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_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.