Table of Contents

Update existing data

Update is for binary data that already exists. You give it a path such as root.value.flags, and it locates the corresponding byte range before writing the replacement.

Use an update when:

  • the field's position and size are already defined by the stored layout;
  • bytes before and after the field must remain where they are; and
  • the destination is a readable, writable, seekable stream.

Do not use it to insert data, grow the stream, relocate following fields, or rebuild a variable-size object. Use Serialize or Write to create new output.

Patch one nested field

The executable example uses:

struct item {
    uint16 id;
    uint8 flags;
};

struct root {
    item value;
};

Two prefix bytes occur before the root. The caller sets the stream position to 2, so that position becomes the starting point for root:

private static void PatchField()
{
    var layout = new CStruct("struct item { uint16 id; uint8 flags; }; struct root { item value; };");
    using var stream = new MemoryStream([0xEE, 0xEE, 0x34, 0x12, 0x01]);
    stream.Position = 2;
    layout.Update(stream, "root.value.flags", (byte)0xA5);
    SequenceEqual([0xEE, 0xEE, 0x34, 0x12, 0xA5], stream.ToArray());
    Equal(2L, stream.Position);

    byte[] before = stream.ToArray();
    Throws<CStructWriteException>(() => layout.Update(stream, "root.value.flags", 999));
    SequenceEqual(before, stream.ToArray());
    Equal(2L, stream.Position);
}

The initial stream is:

absolute offset   0    1    2    3    4
bytes            EE   EE   34   12   01
                       └─ id ─┘  flags
root offset                       2

After updating root.value.flags to 0xA5, the bytes are:

EE EE 34 12 A5

The prefix and id are unchanged, the stream length is unchanged, and the caller-visible position returns to 2.

How validation protects existing data

The method separates the work into two phases:

  1. It follows the path and prepares the replacement in bounded temporary storage.
  2. Only after path, type, range, shape, pointer, union, and configured-limit checks succeed does it copy the changed byte ranges to the destination.

In the example, replacing an eight-bit flags field with 999 cannot succeed. The method throws CStructWriteException, and the test confirms that every destination byte and the stream position stayed unchanged.

This protection covers failures CStructSharp can detect before the commit. It cannot make every possible Stream transactional. A disk, network, or custom stream may accept part of the final commit and then throw. In that case, the accepted prefix may remain changed. If the destination needs storage-level atomicity, use a transactional storage system or write a complete replacement elsewhere and swap it through a mechanism provided by that system.

Update asynchronously

UpdateAsync is the awaitable form. It needs a stream that can seek: the region from the current position is read into a buffer, the update runs over that buffer with the same validation as Update, and only the runs of bytes that changed are written back at their positions - a one-field update is one small write - before the position returns to the origin. A rejected replacement writes nothing. As in the span form, a stored absolute pointer address counts from the region's origin.

private static async Task UpdateAsyncExample()
{
    var layout = new CStruct("struct header { uint16 kind; uint32 length; };");
    using var stream = new MemoryStream([0x02, 0x00, 0x06, 0x00, 0x00, 0x00]);

    // Only the two bytes of 'kind' are written back; the position returns to the origin.
    await layout.UpdateAsync(stream, "header.kind", (ushort)3);
    SequenceEqual([0x03, 0x00, 0x06, 0x00, 0x00, 0x00], stream.ToArray());
    Equal(0L, stream.Position);
}

Paths, strings, unions, and pointers

An indexed path such as root.items[2] updates one array element with the element's codec. A terminated string (or an array ended by an all-zero element) can be replaced only by a value of the same encoded length: the update does not shift later fields to make room, and a replacement that would move them is rejected before anything is written - Update changes the extent of a terminated value and would move the fields that follow .... Serialize a new buffer when the length changes.

A path selecting one union member changes only that member's byte range. Replacing a whole union clears its storage before writing the selected member by default, preventing bytes from an older larger member from surviving.

For pointers, .address changes the stored pointer number. .value follows one pointer level and updates the target. Pointer traversal has separate read limits because locating the destination may itself read untrusted data.

Troubleshooting

If an update fails, check these in order:

  1. The stream supports reading, writing, and seeking.
  2. Its position points to the start of the root object.
  3. The path begins with the correct case-sensitive root name.
  4. Runtime array variables match the values used when the data was written.
  5. The replacement has the exact scalar, collection, struct, enum, union, or pointer shape required by the path.
  6. The replacement fits the existing extent and the configured traversal and write limits.

InvalidPath points to selection, ReadFailed or ReadLimitExceeded points to locating the stored target, and WriteFailed or WriteLimitExceeded points to the replacement.

Read Paths and selection for the complete path syntax and Writing and updating for every union, pointer, and budget rule.