LVGL File-System Drivers: Translate Short Reads and Seek Failures Correctly

A storage adapter must report two independent facts: whether an operation succeeded and how many bytes it actually transferred. A successful short read is not a complete header, and a failed seek must not become a successful read from the wrong offset. Qualify these contracts below the image decoder before diagnosing an asset-format problem.

Applicability: this guide pins LVGL v9.3.0, a custom read-only binary-file adapter and an initial cache_size of zero. It separately identifies limitations of the tagged stdio adapter and cached wrapper. The harness cases are proposed tests, not executed results. Filesystem durability, image caching and logical path packaging are outside scope.

A ten-byte file produces a two-byte short read at position eight, zero bytes at EOF, and an explicit seek-failure branch; exact header parsing requires status and length.
Original proposed adapter-conformance fixture. No execution is claimed. The uncached configuration avoids assuming error propagation from the tagged cached-read implementation.

1. Define the callback result contract

The tagged filesystem header gives read_cb both a requested byte count and an output count. Its return value is an lv_fs_res_t status. Initialize the count on every path, never report more than requested, and preserve the exact number of bytes placed in the destination.

For the custom adapter, define EOF without an underlying error as OK with a short count, including zero when already at EOF. Map a backend I/O failure to an appropriate non-OK status and preserve a diagnostic cause separately. Do not pad missing bytes and report the request as fulfilled.

If a decoder needs an exact-length header, its caller-side acceptance requires both OK and the expected count. If a higher-level reader permits repeated partial reads, require forward progress and stop on zero bytes or error. An unbounded retry loop at EOF can freeze the UI.

2. Use a tiny file with unmistakable positions

Prepare a hypothetical ten-byte fixture containing values 0 through 9. Fill the destination with a sentinel before every operation. Keep the file immutable and use separate handles for independent cases. The initial uncached wrapper passes the callback’s status and actual count through; log both wrapper and backend observations.

Starting state and request Expected successful observation Purpose
Position 0, read 4 Count 4, bytes 0–3, next position 4 Ordinary transfer
Position 8, read 4 Count 2, bytes 8–9, untouched sentinel tail Short read at EOF
Position 10, read 4 Count 0, destination unchanged EOF without invented data
Position 3, read 0 Count 0, position unchanged Zero-length contract
Seek to 2, then read 3 Bytes 2–4 Position correctness

Inject a read failure before any byte is delivered, then a controlled partial-transfer failure in the mock backend. Record which bytes are valid, the error and whether that backend’s position remains trustworthy. Consumers must not continue parsing merely because the destination contains some plausible bytes.

3. Audit the underlying library, not just its wrapper

For C stdio, WG14 draft N1570 specifies fread’s element count, which can be short on EOF or error. With element size one, the count corresponds to bytes. Inspect the stream’s error indicator rather than treating a negative cast of the count as a reliable error test. A read error can leave the stream position indeterminate.

The LVGL v9.3.0 stdio implementation calls fread and tests a signed cast of its count; it does not inspect ferror. Its seek callback calls fseek but returns OK without checking the result, and its tell callback assigns ftell’s result to an unsigned output. These exact-version behaviors are reasons to audit or replace the adapter when error fidelity matters.

In the custom implementation, check fseek’s return and check ftell for failure before conversion. Validate that a successful position is representable by LVGL’s output type. Use binary mode for byte-offset assets. Keep these changes reviewable as adapter behavior; do not claim the stock tagged implementation already provides them.

4. Preserve seek semantics and failure visibility

Map SET, CUR and END explicitly instead of relying on enum values matching the backend. The LVGL position argument is unsigned. Do not smuggle a negative relative offset through a cast; calculate a checked absolute position when backward movement is needed and supported.

Make the mock backend reject one chosen seek without moving its cursor. Assert that the uncached public seek returns the error and that the test does not proceed as if repositioning succeeded. That unchanged-cursor behavior belongs to this mock contract; do not assume every real failed seek has identical state guarantees.

Test zero-offset END only if the selected backend supports it. Follow each successful seek with tell and a known-byte read. Include range rejection near the adapter’s supported size limit, using checked arithmetic rather than allocating a huge fixture simply to reach an overflow case.

5. Treat open and close as separate failure boundaries

The v9.3.0 wrapper distinguishes an unknown driver from a backend open failure: no registered driver returns NOT_EX, whereas an ordinary open_cb returning null produces UNKNOWN. The callback returns a handle, not an lv_fs_res_t, so do not promise a specific missing-file status through that route.

Keep the detailed backend error in the adapter’s diagnostic record. Close only successfully opened handles. Inject close failure separately: the tagged wrapper clears its file fields and releases its cache after calling close_cb even when that callback reports an error. A retry through the cleared LVGL handle is not a supported recovery strategy.

Define how the backend resolves any resources remaining after a close error. Count live handles in the harness and verify successful opens have exactly one terminal ownership outcome. Do not confuse an error message with proof that the native handle was released.

6. Requalify the cached path explicitly

The filesystem guide explains that caching changes backend call sequences. Public seeks can update a logical position, and reads can trigger backend seeks or larger reads. Therefore, callback invocation counts should not be copied from the uncached fixture.

There is an additional exact-version limitation: v9.3.0’s lv_fs_read_cached calls seek_cb in its read paths without checking the seek result. A correct callback alone cannot guarantee that this wrapper reports the failed repositioning. Keep caching disabled for this qualified error-propagating configuration, or qualify a reviewed wrapper correction before enabling it.

Retain the fixture bytes, callback trace, status/count expectations, effective cache setting and fault-injection points. After adapter conformance passes, run the real decoder against a complete asset and truncated variants. The release evidence should show that missing bytes and failed positioning become explicit failures rather than believable but incorrect pixels.

Similar Posts