Haiku BFile: Open Modes, Truncation Safety, and Reliable I/O
Use Haiku BFile with explicit open modes, initialization checks, partial-transfer handling, and clear ownership across file operations.
BFile is Haiku’s Storage Kit object for file data. It derives from BNode and BPositionIO, so it combines node-level access with sequential and offset-based byte I/O. The useful production lesson is that opening a file is a state transition with destructive flags, a status result, a lifetime, and a byte-transfer contract. A valid C++ BFile object does not prove its underlying file opened successfully, and a successful Write() does not necessarily mean every requested byte was written.
Treat the openMode argument as an explicit policy. B_READ_ONLY, B_WRITE_ONLY, and B_READ_WRITE describe access; B_CREATE_FILE requests creation when absent; B_FAIL_IF_EXISTS makes creation fail if the path already exists; B_ERASE_FILE truncates an existing file to zero length; and B_OPEN_AT_END positions the file at its end. These choices belong in the call site or a narrowly named helper, not in a generic default that silently erases data.
Check initialization on every open path
BFile can be constructed from a path, an entry_ref, a BEntry, or a directory-relative path. Each form resolves a different name or object context. Use an entry_ref or BEntry when the caller already has that identity; use a path when the operation intentionally targets a current namespace path. Do not convert to a string path unnecessarily and then assume that the object still denotes the same file.
status_t ReadConfiguration(const char* path)
{
BFile file(path, B_READ_ONLY);
status_t status = file.InitCheck();
if (status != B_OK)
return status;
char buffer[4096];
ssize_t bytes = file.Read(buffer, sizeof(buffer));
if (bytes < 0)
return static_cast<status_t>(bytes);
return ParseConfiguration(buffer, static_cast<size_t>(bytes));
}
The parser receives exactly the number of bytes read, not the size of the stack buffer. Do not assume text files are NUL-terminated or that one read returns the complete file. If the format needs the entire document, use a bounded loop, check the file size against an application limit, handle short reads, and report truncation or malformed input separately from a valid empty file.
The path constructor and SetTo() have error results that should be checked through InitCheck() immediately. Do not call read, write, seek, or metadata operations on an uninitialized object and interpret the resulting error as ordinary end-of-file. On a reopen, check SetTo()’s returned status before using the new state. Keep the failed path and status in diagnostics, while avoiding logging sensitive file contents.
Make destructive flags visible in code review
The most dangerous open-mode mistake is adding B_ERASE_FILE to a shared helper whose callers believe they are opening an existing file for an update. That flag truncates an existing file at open. B_CREATE_FILE by itself expresses create-if-missing; pair it with B_FAIL_IF_EXISTS when overwriting a path would be incorrect. Use the narrowest access mode that permits the operation.
status_t CreateNewReport(const char* path, const void* data, size_t size)
{
BFile file(path, B_WRITE_ONLY | B_CREATE_FILE | B_FAIL_IF_EXISTS);
status_t status = file.InitCheck();
if (status != B_OK)
return status;
return WriteAll(file, static_cast<const uint8*>(data), size);
}
The example assumes WriteAll() handles partial writes and rejects zero progress. It deliberately omits B_ERASE_FILE, so an existing report is not silently destroyed. If replacement is required, make it a separately named operation with an explicit backup, temporary-file, or versioning policy. A write that truncates then fails can leave the user with neither the old nor the new content.
Creation flags are not a complete security policy. Validate the parent directory and intended destination, account for symbolic links and concurrent path changes, and use APIs appropriate to the trust boundary. A name checked earlier can refer to a different object by the time it is opened. Revalidate identity and permissions at the point where the operation is committed, and avoid following links when the workflow must stay within a trusted directory.
Treat reads and writes as byte counts
Read() and Write() return a signed byte count or an error code. A nonnegative short result is not automatically a complete transfer. For a parser, loop until the required byte count is obtained or EOF/error occurs. For a writer, advance the source pointer by the number of bytes accepted and stop on an error or zero progress. Keep requested length in size_t, but compare returned counts carefully before converting; negative errors must not become enormous unsigned values.
status_t WriteAll(BPositionIO& output, const uint8* data, size_t length)
{
size_t offset = 0;
while (offset < length) {
ssize_t written = output.Write(data + offset, length - offset);
if (written < 0)
return static_cast<status_t>(written);
if (written == 0)
return B_IO_ERROR;
offset += static_cast<size_t>(written);
}
return B_OK;
}
This helper treats zero progress as failure to avoid an infinite loop. The caller still has to define what partial output means: a truncated configuration file may need deletion or recovery from a backup, while a streaming upload may report the number of bytes already sent. WriteAll() does not make the overall operation atomic or durable. If you need crash-safe replacement, design a separate write-to-temporary, validate, flush/sync, and rename workflow using documented filesystem semantics for the target environment.
BPositionIO also provides ReadAt() and WriteAt() so each operation names an offset rather than sharing a mutable seek cursor. Those operations can simplify independent reads, but they still return counts/errors and do not make an underlying file immutable. Validate offsets and length arithmetic against the file size; concurrent truncation or replacement remains possible.
B_OPEN_AT_END is a positioning choice made when the file is opened, not a transaction protocol. If multiple writers can append concurrently, define how records are framed and serialized instead of assuming that several related writes form one indivisible record. A partial record should be detectable and recoverable by the reader, and append failures should be surfaced to the caller with the number of bytes already committed where that information matters.
Keep object lifetime and node operations separate
A BFile instance encapsulates an open file and releases that state when it is reset or destroyed. Keep it alive for the duration of the transfer, and do not store a pointer to a temporary BFile in a deferred job. If work crosses threads, either transfer ownership through an explicit object or reopen by a stable reference and handle disappearance. File descriptors are finite resources; close them promptly rather than retaining one open for the lifetime of a UI row.
Because BFile derives from BNode, it can also expose attributes and node-level operations. Those are not the same as writing bytes into the data stream. A file can have a data fork and filesystem attributes, and the selected filesystem may not support attributes in the same way. Check the return status for each attribute operation and do not make application correctness depend on optional metadata unless the target volume’s capabilities are known.
Do not assume that two separately opened BFile objects coordinate through the same seek position or provide application-level mutual exclusion. If shared state must be serialized, use an explicit lock or a single owner and document whether it protects a thread, process, or filesystem operation. Keep a lock around the smallest critical state transition; never wait for user input or network work while holding it.
Test failure and recovery behavior
Test a missing file, permission denial, a directory passed where a file is expected, creation collision, B_ERASE_FILE behavior on a disposable fixture, read-only media, zero-length input, short transfer, disk-full behavior, and a file removed during use. Verify that failures remain visible to the caller and do not get converted into success-shaped empty data. For a write workflow, test interruption after partial output and confirm the documented recovery path restores or identifies the incomplete result.
At the boundary between UI and storage, report the path or display name safely and preserve the status code for logs and diagnostics. Avoid automatically retrying destructive opens: a retry with B_ERASE_FILE repeats truncation, not recovery. Use a distinct operation for create, overwrite, append, and atomic replacement so reviewers can see the intended behavior without reconstructing a bitmask from a generic helper.
BFile is straightforward when its contracts stay explicit: resolve the intended object, check open state, choose non-surprising flags, handle partial byte counts, and make failure recovery part of the operation design.
Related:
- BPositionIO in Haiku: Offset-Based Reads Without Shared Seek State
- How to Create BFS Attributes and Indexes for Fast Haiku Queries
Sources: