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.