Compression transforms bytes without changing their ownership contract
Compression is a byte transformation, so its input still needs an encoding if the application starts with text. The lesson explicitly converts rune rune rune to UTF-8, compresses those fourteen bytes through GZipStream, then reconstructs the original bytes with another real framework stream. Both compressed and decompressed storage live in memory, and the input is trusted classroom data. This keeps the experiment focused on wrapper order, completion and counted output. A successful round trip demonstrates that this pipeline recovers these bytes; it does not establish a production policy for accepting arbitrary archives or protecting a service from hostile compressed content.
Complete the compressor before beginning the decoder
The compressor wraps a memory destination and leaves that destination open when disposed. Its scope ends before the destination is rewound for reading, allowing the gzip representation to be finalized before a decoder consumes it. Flushing an active compressor is not a substitute for completing its lifetime and producing a finished member. The sample then assigns zero to the packed stream's position and creates a decompressor that reads from that point. Without the explicit rewind, the source cursor would still be after the produced compressed bytes. Completion, ownership and positioning are separate operations with separate reasons in this sequence.
Trace the restored text, equality and byte count
The decompression loop offers a four-byte buffer on each read and processes only the prefix indicated by the returned count. It appends that prefix to the restored memory stream after checking its accepted-output budget. Once a nonempty read request returns zero, the loop finishes. Decoding the restored bytes produces the first line, rune rune rune. Comparing the restored array with the original bytes produces True, and printing its length produces 14. The sample intentionally does not predict compressed size or exact compressed bytes, which are not necessary to verify the observable round-trip property this exercise teaches.
Name exactly what the classroom limit controls
Before each append, the loop checks whether the accumulated length plus the new count would exceed 128 bytes. That prevents this accumulator from accepting more than its stated decompressed-output budget. It does not bound every internal allocation, the compressed input size, elapsed work, decompression ratio or downstream parser complexity. The entire compressed example is already in a memory stream before the loop starts. A production ingestion design needs additional resource and format decisions; this classroom check should never be advertised as a complete decompression-bomb defense. Its narrower benefit is making an output budget explicit and testable.
Treat compressed reads as ordinary count-returning operations
A decompressor can produce fewer bytes than the requested buffer size before the logical end of the decompressed sequence. The loop therefore continues after any positive result and appends only that result's prefix. It does not assume that four offered slots mean four available bytes, and it does not use the compressed source's length as the decompressed length. Invalid or truncated gzip input can raise an exception instead of yielding a valid completed document. Let a caller distinguish that failure from a successful empty payload rather than catching every error and publishing whatever prefix happened to be produced first.
Change the payload independently from the accepted budget
For the first exercise, replace the trusted text with rune and predict all three output lines, keeping the same encoding and compression pipeline. Explain why the uncompressed byte count changes even though no expectation about compressed size is needed. For the second, restore the original text and reduce the accepted-output budget to thirteen bytes. Add a narrow InvalidDataException handler that reports rejection rather than treating an accumulated prefix as a successful result. Predict whether the original fourteen-byte document can be accepted, regardless of how the decompressor divides it across individual reads.
- Hint for the shorter text: the restored line is rune, equality stays True and the restored length is four.
- Hint for the thirteen-byte budget: reject before appending a prefix that would cross the limit; do not print a successful completed document.
Review transformations and resource boundaries together
Draw the original bytes, compressor, packed stream, decompressor and restored stream as distinct stages. Mark which object owns each stream, when each wrapper completes, and which byte sequence each length describes. Then identify the one limit actually enforced by the program: accepted bytes in the restored accumulator. This review prevents several common mistakes, including reading before compression finishes, forgetting to rewind, comparing character count with compressed length and claiming broad hostile-input protection from a small output check. The same staged reasoning is useful later when HTTP content arrives compressed, but this lab itself sends no request and depends on no external archive.
- Complete a compression wrapper before consuming its finished representation.
- Continue after positive short reads and append only the valid prefix.
- Describe an output budget precisely without expanding it into a general security claim.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
byte[] original = Encoding.UTF8.GetBytes("rune rune rune");
using var packed = new MemoryStream();
using (var compressor = new GZipStream(packed, CompressionLevel.Optimal, leaveOpen: true)) { compressor.Write(original); }
packed.Position = 0; using var restored = new MemoryStream();
using (var decoder = new GZipStream(packed, CompressionMode.Decompress, leaveOpen: true)) {
byte[] buffer = new byte[4]; int count;
while ((count = decoder.Read(buffer, 0, buffer.Length)) != 0) { if (restored.Length + count > 128) throw new InvalidDataException("Classroom output limit"); restored.Write(buffer, 0, count); }
}
Console.WriteLine(Encoding.UTF8.GetString(restored.ToArray())); Console.WriteLine(original.SequenceEqual(restored.ToArray())); Console.WriteLine(original.Length);
Expected output
rune rune rune True 14
Dispose the compressor to finish its format, and describe this small output limit as a teaching contract only.