Haiku BStringList: Ordered Collections, Copy Semantics, and Joining
Manage Haiku text collections with BStringList ordering, checked mutation, search, sorting, joining, flattening, and explicit persistence schemas.
BStringList is a mutable ordered collection of BString values. It provides append and indexed insertion, removal, replacement, search, sorting, indexed retrieval, and joining. It is useful for a list of search paths, command-line arguments, line-oriented metadata, or other simple string sequences where order matters.
It is not a map, a set, or a CSV/JSON serializer. Duplicate values are allowed unless the application prevents them; sorting changes order; joining does not quote or escape separators; and a flattened representation is not automatically a stable application file format. Model those semantics explicitly rather than treating a collection helper as a schema.
Add values and check every mutation
Add() returns bool and can append a BString or insert one at an index. The current implementation makes a BString value copy for each item and shares its internal string data when possible; the list does not take ownership of the caller’s BString object or retain a pointer to that wrapper. The caller can mutate or destroy its wrapper afterward without invalidating the list’s string value.
#include <String.h>
#include <StringList.h>
BStringList arguments;
if (!arguments.Add(BString("--output")))
return B_NO_MEMORY;
if (!arguments.Add(BString("report.txt")))
return B_NO_MEMORY;
for (int32 index = 0; index < arguments.CountStrings(); ++index) {
BString argument = arguments.StringAt(index);
ProcessArgument(argument.String());
}
The example checks every insertion and copies each retrieved value into a local BString. StringAt() returns a BString value; index validity remains the caller’s responsibility. Before calling it, require 0 <= index < CountStrings(). For very large or externally supplied input, impose a maximum count and value length so a list cannot grow without bound.
Use Add(value, index) only after validating the insertion position against the list’s current count. Insertion changes every later index. If order is semantically significant, avoid keeping long-lived numeric indexes across mutations; store a stable key alongside the list or locate the item again with IndexOf() after edits. IndexOf() returns -1 when there is no match.
Remove and replace without ambiguity
Remove(index) returns the removed BString; the current implementation returns an empty string for an invalid index. Because a valid list item can itself be empty, the return value alone cannot distinguish successful removal of "" from an out-of-range request. Validate the index first. The overload Remove(index, count) returns bool; validate nonnegative index/count and the intended range before mutating, especially when values come from a UI selection or external message.
The overloads that remove by string or another BStringList return whether something was removed. ignoreCase controls case-insensitive matching for those operations. Do not treat this as locale-aware comparison or Unicode normalization. If identifiers have a domain-specific case-folding or normalization rule, normalize/compare them at the model boundary and test that policy explicitly.
Replace(index, value) also returns bool. Keep a mutation transactional when a larger application model depends on multiple positions: validate the full requested operation first, build a replacement list or snapshot if necessary, then publish the result. A sequence of individually valid removals can still leave a partially edited list if a later index was calculated against the pre-mutation positions.
For repeated pruning, remove in descending index order so earlier removals do not shift indexes that are still pending. Alternatively, build a new list with the values to keep and swap it into the model after all checks pass. Prefer the simpler method that makes ownership and failure behavior clear; do not mutate a list from inside a callback that is simultaneously iterating it unless the API explicitly supports that pattern.
Sort order is a product decision
Sort() mutates the list and supports the default comparison with optional case-insensitive behavior, plus custom comparison callbacks. The default argument is case-sensitive in the current header. Sorting destroys the prior sequence order, so sort only when order is no longer a user choice or when the UI clearly communicates that it has sorted the items.
If the list contains filesystem paths, package names, or locale-sensitive words, the generic string sort may not match the desired domain order. Use a comparator that implements the application’s semantics and a deterministic tie-breaker. A comparator must provide a consistent ordering; contradictory results can make a sorting algorithm behave unpredictably. If exact reproducibility matters across locales and releases, define the collation locale and comparison rules rather than relying on the current system default.
Do not assume that Sort() is stable unless the API explicitly documents stability for the target release. When equal keys need to preserve their prior relative order, include a stable sequence number in your application records or implement a tested stable ordering strategy outside BStringList.
Move(fromIndex, toIndex) and Swap(indexA, indexB) allow deliberate reordering. Validate both endpoints and update any selected-row model that references indexes. In a UI, emit a model-level “item moved” operation and preserve the selected item’s identity rather than assuming the same numeric index continues to refer to it.
Join creates text, not a structured encoding
Join(separator) concatenates the list values with a separator and returns a BString. It does not escape occurrences of the separator, quotes, newlines, or other syntax. Joining {"a,b", "c"} with a comma produces text that cannot be reliably split back into the original two values without an escaping convention.
BStringList fields;
fields.Add(BString("north"));
fields.Add(BString("south"));
BString display = fields.Join(" / ");
This is appropriate for display text or a format whose grammar explicitly defines plain joining. For CSV, shell arguments, JSON, line protocols, or command construction, use a serializer that quotes and escapes according to that format. Never use a simple joined string as a substitute for an argument vector if values can contain whitespace or shell metacharacters. If a separator can appear in values, round-tripping requires an explicit escape or length-prefix schema.
The optional separator-length argument is a byte count; the implementation measures the separator accordingly. Ensure the pointer is non-null and valid for the call, and do not pass a character count for a UTF-8 separator. As with other BString APIs, distinguish bytes from visible characters.
Flattening and durable schemas
BStringList implements BFlattenable, exposing a type code and flattened representation for Haiku’s typed data mechanisms. This can be useful for message/archive interoperability when both sides agree on the type and target API. It does not mean a raw flattened byte sequence is a documented, long-term interchange file format for every application.
For durable settings, store a versioned list of named string fields or a clearly defined sequence representation. Check read and write status, reject unreasonable item counts and lengths, and preserve the distinction between an empty list and a list containing one empty string. If legacy files were written using Join(), keep the historical delimiter and escaping rules in a migration; do not retrofit new assumptions onto ambiguous old text.
On loading, validate each string independently. A successfully unflattened list can still violate product constraints such as allowed path roots, unique names, or a maximum number of entries. Validation should be deterministic and report which item failed. Avoid silently sorting or deduplicating unless the user-visible data model explicitly says order and duplicates do not matter.
Threading and snapshots
Do not mutate one BStringList instance concurrently from multiple threads without synchronization. The fact that each element is a BString value does not make the containing list’s structural updates race-safe. For a worker task, copy or build a snapshot under the model’s lock, release the lock before long operations, and publish a complete replacement through the owning thread. Avoid holding a UI lock while sorting or reading a large file into a list.
If the application stores a selection by list index, carry the item identity along with the async request. When the worker returns, resolve that identity against the current list; the same index may now refer to another string after a concurrent insert or sort. A copied list provides a snapshot of collection structure, but the app still needs a rule for whether that snapshot is current enough to apply.
Operational tests
Test empty list, one empty value, duplicates, insertion at beginning/end, invalid indexes, removal of the first/middle/last item, and replacing a value with another identical value. Verify that IndexOf() returns the expected case-sensitive and ignore-case result. Exercise sorting with upper/lowercase, non-ASCII UTF-8, duplicate strings, and a custom comparator.
Test Join() with a separator inside a value, embedded newline, empty strings, and a UTF-8 separator. Confirm the output is only used as display or the intended escaped format. Round-trip flattening only where required and add explicit file-schema tests for upgrade behavior. Fuzz or property-test index calculations if they are derived from untrusted counts.
BStringList is a convenient ordered collection, but its simple API leaves important semantics to the caller: uniqueness, collation, escaping, persistence compatibility, thread ownership, and stable item identity. Make those policies explicit and test mutations as model operations, not just calls that happen to return true.
Related:
- Haiku BString: UTF-8 Length, Indexing, and Mutation
- Haiku find_directory and BPathFinder: Resolve Paths at Runtime
Sources: