Table of Contents

Inspect and edit a small binary file

This advanced walkthrough combines the header lesson, arrays, runtime variables, and updates. You need basic file I/O and the .NET 10 SDK to run the repository example.

Define the format before writing code

The teaching file uses packed little-endian fields. It contains a signature, version, record count, and records. Each record contains a two-byte id and a one-byte flags value. This example accepts 2 through 32 records.

Offset Bytes in the fixture Meaning
0โ€“1 43 53 ASCII signature CS
2 01 Version 1
3 02 Two records
4โ€“6 01 00 10 Record 0: id 1, flags 16
7โ€“9 02 00 20 Record 1: id 2, flags 32

The application validates the signature and version. CStructSharp does not know that those values identify this format. The application also checks the count and exact length before supplying COUNT as a runtime variable. A field named count does not automatically bind an array expression.

Run and inspect the complete program

dotnet run --project docs/examples/CStructSharp.Docs.Examples.csproj -c Release -- edit-file

The example creates a temporary fixture file, reads it, and removes it when finished. It patches record 1's flags to A5 in a copy, verifies that the other nine bytes are unchanged, and rejects both a truncated file and an excessive count. Its printed output includes 435301020100100200A5 and PASS edit-file.

// Generated from executable documentation examples. Edit the source region, then regenerate.
using System;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Buffers;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using CStructSharp;
using CStructSharp.Codecs;
using CStructSharp.Diagnostics;
using CStructSharp.Introspection;
using CStructSharp.Values;

internal static partial class Program
{
    public static void Main()
    {
        EditFile();
        Console.WriteLine("PASS edit-file");
    }

    private static void EditFile()
    {
        // Teaching format: signature C S, version 1, count, then count records (uint16 id, uint8 flags).
        byte[] fixture = [0x43, 0x53, 1, 2, 1, 0, 0x10, 2, 0, 0x20];
        string file = Path.Combine(Path.GetTempPath(), "cstructsharp-example-" + Guid.NewGuid().ToString("N") + ".bin");
        try
        {
            File.WriteAllBytes(file, fixture);
            byte[] input = File.ReadAllBytes(file);
            var headerLayout = new CStruct("struct header { uint8 signature[2]; uint8 version; uint8 count; };");
            var recordsLayout = new CStruct("struct record { uint16 id; uint8 flags; }; struct data { record records[COUNT]; };");

            byte[] Patch(byte[] data)
            {
                if (data.Length < 4 || data[0] != 0x43 || data[1] != 0x53 || data[2] != 1)
                {
                    throw new InvalidDataException("Expected a CS file, version 1, with a complete header.");
                }

                int count = (byte)headerLayout.ReadValue(data.AsSpan(), "header.count")!;
                if (count is < 2 or > 32 || data.Length != 4 + count * 3)
                {
                    throw new InvalidDataException("Expected 2 through 32 complete records and no trailing data.");
                }

                var variables = new Dictionary<string, int> { ["COUNT"] = count };
                using var stream = new MemoryStream((byte[])data.Clone());
                stream.Position = 4;
                Equal((ushort)2, (ushort)recordsLayout.ReadValue(stream, "data.records[1].id", variables)!);
                stream.Position = 4;
                recordsLayout.Update(stream, "data.records[1].flags", 0xA5, variables);
                return stream.ToArray();
            }

            byte[] output = Patch(input);
            SequenceEqual([0x43, 0x53, 1, 2, 1, 0, 0x10, 2, 0, 0xA5], output);
            SequenceEqual(input[..9], output[..9]);
            Throws<InvalidDataException>(() => Patch(input[..^1]));
            byte[] excessive = (byte[])input.Clone();
            excessive[3] = 255;
            Throws<InvalidDataException>(() => Patch(excessive));
            Console.WriteLine(Convert.ToHexString(output));
        }
        finally
        {
            File.Delete(file);
        }
    }

    private static void Equal<T>(T expected, T actual)
    {
        if (!EqualityComparer<T>.Default.Equals(expected, actual))
        {
            throw new InvalidOperationException($"Expected '{expected}', received '{actual}'.");
        }
    }

    private static void SequenceEqual(byte[] expected, byte[] actual)
    {
        if (!expected.AsSpan().SequenceEqual(actual))
        {
            throw new InvalidOperationException(
                $"Expected {Convert.ToHexString(expected)}, received {Convert.ToHexString(actual)}.");
        }
    }

    private static void Throws<TException>(Action action)
        where TException : Exception
    {
        try
        {
            action();
        }
        catch (TException)
        {
            return;
        }

        throw new InvalidOperationException($"Expected {typeof(TException).Name}.");
    }
}

Why the stream position matters

The records begin at offset 4. Before selecting data.records[1].flags, the program sets Position to 4. The selected field is then at 4 + 3 + 2 = 9. The count dictionary describes this operation; it does not modify the reusable compiled layout.

The program validates all required records before patching. It checks untouched bytes after patching rather than assuming a successful return proves preservation. For a real file, decide how to save the resulting copy and how to recover from a physical storage failure. Library validation and operating-system write failures have different recovery behavior.

Try a change

Change count to 255 but keep the fixture length. The application rejects it before traversal. Remove the final byte instead: the exact-length check rejects the truncated record. To add a record, construct a new complete file with an updated count; a fixed-field update cannot move following data.

The browser does not expose the runtime-variable dictionary. The browser inspector walkthrough uses a fixed two-record variant and explicitly validates that count. See runtime variables for formats whose record count varies.