Two system administrators reviewing terminal output and handwritten validation notes at a dual-monitor workstation.

gzip --list Does Not Count Every Concatenated Member

Fri, Oct 23, 2026

A local gzip file can pass an integrity test, decode cleanly to 15 bytes, and still have gzip --list report an uncompressed size of 5 bytes. That result is not evidence that GNU gzip lost ten bytes. It is the documented consequence of asking a member-oriented listing field to answer a whole-decoded-stream question.

The controlled example in this playbook uses two authored payloads:

A = b"A,10\nA,11\n"   # 10 bytes
B = b"B,20\n"         # 5 bytes

Compress A and B separately, concatenate the two gzip members, and the complete decoded stream is A+B: 15 bytes. GNU gzip documents both sides of the apparent contradiction: concatenated gzip members are decompressed in sequence, while --list reports the uncompressed size and CRC of the last member only when the file contains several members.

That distinction changes the admission design.

For this playbook, a local artifact is not approved because gzip -t succeeds, because a listing field equals an expected number, because a decoder created a file, or because the compressed representation has a particular member count. The acceptance contract is the exact decoded byte sequence. Promotion requires successful integrity and decode exits plus the independently expected decoded length and SHA-256.

The laboratory is intentionally bounded: GNU gzip 1.13 and CPython 3.13.5 in the supplied October 23, 2026 preparation run, LC_ALL=C, a minimal subprocess environment without GZIP, tiny self-authored data, owned temporary paths, no network, and no shell pipeline. The supplied eight-case preparation run is the execution evidence discussed below; it is not relabeled as a new run performed for this article.

State the acceptance contract and freeze the validation environment

The first reliability decision is not which gzip command to run. It is which representation the consumer has authorized.

Here the answer is deliberately narrow:

Accepted object: the complete decoded byte sequence A+B.

The contract does not say that the candidate must contain two gzip members. It does not require the first member to have been compressed in one process and the second in another. It does not require a particular DEFLATE encoding, gzip header, compression ratio, timestamp, or compressed-file SHA-256. It does not make member provenance part of admission.

That is why an independently encoded single gzip member containing A+B will eventually be accepted, while a structurally valid two-member file containing B+A will be rejected.

This is a representation question before it is a gzip question. A related Refonte Learning article on choosing the byte representation in HTTP downloads applies the same contract-first discipline to a networked encoded-versus-decoded boundary; this playbook stays entirely offline and local.

Three properties must remain separate throughout the handoff.

Integrity asks whether the gzip candidate can be processed successfully under the chosen decoder and its member checks. In this laboratory, gzip -t and gzip -dc must both exit zero. RFC 1952 defines gzip files as a series of members appearing one after another, and each member carries a trailer containing CRC32 and ISIZE fields.

Completeness asks whether the successful decode produced the amount of data authorized by the separate expectation. For A+B, that number is 15 bytes. A successful gzip integrity test alone cannot answer that question: a valid file containing only B is a perfectly plausible gzip object, but it is incomplete for this consumer.

Byte identity asks whether the decoded result is the authorized content, not merely the authorized length. A+B and B+A are both 15 bytes. Length therefore cannot distinguish them. This playbook uses an independently prepared SHA-256 together with the expected length. Python's hashlib documentation specifies the sha256() constructor and digest interface used by the fixture.

The expectation has to precede the candidate. Computing the "expected" SHA-256 from whatever was just decoded would prove only that the decoder output hashes to itself. It would eliminate the independent oracle the gate needs.

There is also a trust boundary here. A SHA-256 digest does not establish who supplied the manifest or whether that supplier was authorized. Manifest authentication is outside this laboratory. If the expected length and digest are absent or not trusted through the surrounding system's established controls, the correct decision is HOLD, not "derive a replacement expectation from the gzip file."

The environment deserves equally explicit treatment. The preparation harness resolves gzip with shutil.which(), then invokes it with:

ENV = {'PATH': os.defpath, 'LC_ALL': 'C'}

Because the child environment is constructed from that two-key mapping rather than inherited wholesale, an ambient GZIP variable is not passed into these subprocesses. LC_ALL=C also makes parsing the tiny gzip -l fixture less dependent on a caller's locale.

Owned temporary paths matter because staged bytes should not be mixed with an unrelated consumer's existing destination. The fixture uses a new TemporaryDirectory for each case and writes owned.gz, staged.bin, and, conditionally, published.bin inside it.

Documentation identity and executable identity should not be collapsed into one version label. The cited documentation is the GNU gzip 1.14 manual, while the preparation executable was GNU gzip 1.13. The manual documents concatenated decoding and the last-member --list limitation.

The same distinction applies to Python. The supplied preparation runtime was CPython 3.13.5. The cited Python 3.13 documentation is labeled 3.13.17. Its relevant API documentation says gzip.compress(..., mtime=0) can be used for creation-time-independent output and that gzip.decompress() supports multi-member gzip data. Those documentation facts do not mean that the preparation runtime was 3.13.17.

Construct an independent manifest and preserve the complete fixture

The laboratory uses deliberately tiny payloads so every relevant byte can be reasoned about directly:

A = b'A,10\nA,11\n'
B = b'B,20\n'
C = b'C,30\n'

A is 10 bytes. B is 5. The approved A+B payload is therefore 15 bytes. C is another 5-byte record used only to prove that adding a structurally valid gzip member does not make the resulting artifact acceptable.

For the authored payloads, the relevant SHA-256 values are:

Payload

Length

SHA-256

A

10

ec3fbf048f04bbade045c58f187ad01d06c4a28700179888c2308bbaf29d6f2e

B

5

bc6a3b29ef65267fedfa5d2f8fce98a611a11dcbb752e21727fdb6660bc05d23

A+B

15

0a8adbb8f207117540c252196eca37083eb708aa8668ed632740ed9c98f030d6

B+A

15

17e8b8d7c53463f5a19536e4f8c45649d88e624360fb52007a8eb38b5445da81

A+B+C

20

b698dd129ae2079cbd31d30888eea1f802f4d598d6a260329704e7245aa5eadb

The important direction of dependency is:

authored expected bytes
        ↓
expected length + expected SHA-256

candidate gzip
        ↓
decode
        ↓
observed length + observed SHA-256
        ↓
compare with independent expectation

Do not reverse those arrows.

Python's gzip documentation describes mtime=0 as the way to request compressed output that does not depend on creation time, which is useful when constructing reproducible fixture members. It also explicitly documents multi-member decompression support.

The preparation fixture should be preserved as a unit rather than reduced to a favorable two-line demonstration. Its negative controls are what show why each acceptance condition exists.

Save this exact source as gzip_lab.py. The source text below corresponds to the supplied SHA-256 1a5651a0f060603b9f7e83ebe616a3707e3283abb1a8b48c8b95dffc76ae8557.

import gzip
import hashlib
import json
import os
import platform
import shutil
import subprocess
import tempfile
from pathlib import Path

if not debug:
    raise RuntimeError('Run without -O; diagnostic assertions are required.')
GZIP = shutil.which('gzip')
if GZIP is None:
    raise RuntimeError('GNU gzip is required.')
ENV = {'PATH': os.defpath, 'LC_ALL': 'C'}
A = b'A,10\nA,11\n'
B = b'B,20\n'
C = b'C,30\n'
sha = lambda b: hashlib.sha256(b).hexdigest()


def command(*args):
    return subprocess.run([GZIP, *map(str, args)], capture_output=True,
                          env=ENV, timeout=5, check=False)


def run(case, archive, expected_payload, expected_listing, expected_decode_ok,
        expected_admission):
    with tempfile.TemporaryDirectory(prefix='gzip-list-lab-') as tmp:
        root = Path(tmp)
        path = root / 'owned.gz'
        path.write_bytes(archive)
        listing = command('-l', path)
        integrity = command('-t', path)
        decoded = command('-dc', path)
        listed_size = None
        if listing.returncode == 0:
            lines = listing.stdout.decode('ascii').strip().splitlines()
            assert len(lines) == 2, lines
            listed_size = int(lines[1].split()[1])
        if expected_listing is not None:
            assert listed_size == expected_listing, (case, listed_size)
        assert (decoded.returncode == 0) == expected_decode_ok, case
        assert (integrity.returncode == 0) == expected_decode_ok, case
        staged = root / 'staged.bin'
        staged.write_bytes(decoded.stdout)
        admitted = (decoded.returncode == 0 and integrity.returncode == 0
                    and len(decoded.stdout) == len(expected_payload)
                    and sha(decoded.stdout) == sha(expected_payload))
        assert admitted == expected_admission, case
        published = root / 'published.bin'
        if admitted:
            staged.replace(published)
            assert published.read_bytes() == expected_payload
        else:
            assert not published.exists()
        return {'case': case, 'listed_uncompressed': listed_size,
                'list_rc': listing.returncode, 'test_rc': integrity.returncode,
                'decode_rc': decoded.returncode, 'decoded_bytes': len(decoded.stdout),
                'expected_bytes': len(expected_payload), 'decoded_sha256': sha(decoded.stdout),
                'expected_sha256': sha(expected_payload), 'admitted': admitted,
                'decode_stderr': decoded.stderr.decode('utf-8', errors='replace')}


if name == '__main__':
    a, b, c = (gzip.compress(x, mtime=0) for x in (A, B, C))
    damaged = bytearray(a + b)
    damaged[-8] ^= 1  # One CRC byte in the last trailer, not a traversal payload.
    cases = [run('G1 single', a, A, len(A), True, True),
             run('G2 concatenated', a+b, A+B, len(B), True, True),
             run('G3 reversed', b+a, A+B, len(A), True, False),
             run('G4 member omitted', b, A+B, len(B), True, False),
             run('G5 member added', a+b+c, A+B, len(C), True, False),
             run('G6 CRC corrupt', bytes(damaged), A+B, None, False, False),
             run('G7 truncated trailer', (a+b)[:-4], A+B, None, False, False),
             run('G8 one equivalent member', gzip.compress(A+B, mtime=0),
                 A+B, len(A+B), True, True)]
    version = command('--version').stdout.decode('ascii').splitlines()[0]
    print(json.dumps({'python': platform.python_version(), 'gzip': version,
                      'source_sha256': sha(Path(__file__).read_bytes()),
                      'contract': 'decoded byte identity, not compressed representation or member count',
                      'cases': cases, 'status': 'PASS_8_CASES'}, indent=2))

The preparation commands were:

gzip --version
python gzip_lab.py > gzip-evidence.json

Do not run the fixture with python -O or PYTHONOPTIMIZE, because its diagnostic assertions are part of the test. The explicit debug guard fails early if assertions have been disabled.

The supplied preparation evidence records CPython 3.13.5, GNU gzip 1.13, the source digest above, and PASS_8_CASES on October 23, 2026. Those statements are attribution to the supplied run, not claims that a new execution was performed while writing this article.

The source digest also has a deliberately limited meaning. It identifies the fixture text. It is not proof that the code is correct, proof that a particular machine executed it, or authentication of the resulting JSON. Keep the source revision, runtime labels, command statuses, manifest identity, and per-case evidence together.

Prove the last-member listing limitation before repairing the gate

Start with G1 because it is the case that makes a faulty validation policy look reasonable.

G1 contains one gzip member encoding A.

The supplied preparation result is uncomplicated:

gzip --list uncompressed: 10
decoded bytes:            10
gzip -t:                   success
gzip -dc:                  success
expected payload:          A
decision:                  ACCEPT_DECODED

For a single member, the listing's uncompressed-size field and the complete decoded length happen to answer the same numerical question.

That agreement is a baseline, not a general rule.

RFC 1952 describes a gzip file as a series of members, with the members appearing consecutively and without an extra container-level total around them. Each member ends with a trailer containing that member's CRC32 and ISIZE. The GNU-specific behavior of --list then matters: GNU gzip documents that, for files consisting of several members, the uncompressed size and CRC reported by --list apply to the last member only.

G2 is the counterexample that should be preserved in a regression test.

Its compressed file is:

gzip(A) || gzip(B)

Its decoded stream is:

A,10\n
A,11\n
B,20\n

That is 15 bytes.

The supplied preparation result is:

gzip --list uncompressed: 5
decoded bytes:            15
gzip -t:                   success
gzip -dc:                  success
decoded SHA-256:           0a8adbb8f207117540c252196eca37083eb708aa8668ed632740ed9c98f030d6
expected SHA-256:          0a8adbb8f207117540c252196eca37083eb708aa8668ed632740ed9c98f030d6
decision:                  ACCEPT_DECODED

The 5 is B's size, because B is the final member. It is not the size of the complete 15-byte decoded payload.

GNU gzip also documents that concatenated compressed files are decoded as their members in sequence. Python's gzip documentation independently describes the same multi-member concept for gzip.decompress(): multiple concatenated gzip blocks can be decompressed as multi-member data.

That makes the operational defect precise. A validator that says:

gzip --list says 5 bytes
manifest says 15 bytes
therefore reject the artifact

is measuring the wrong thing.

The decision for that validator is REPAIR_LIST_GATE.

REPAIR_LIST_GATE is not an exception that waives content validation. It means the control itself is defective because it treats a last-member field as a complete-stream byte counter. G2 still has to pass the real artifact contract: successful test, successful decode, exactly 15 decoded bytes, and the expected SHA-256.

This distinction is useful when recovering historical false rejections. The gzip artifact and the gate can receive different diagnoses. G2 is ACCEPT_DECODED as content while the old size-check implementation is REPAIR_LIST_GATE.

The adjacent operational lesson in reconciling bytes retained across retries is also to judge the complete retained representation rather than inferring it from an intermediate event. That neighboring article concerns curl and retries; no retry or network behavior is part of this gzip laboratory.

There is a second reason not to reinterpret the member trailer as an unlimited total. RFC 1952 defines ISIZE as the original uncompressed input size modulo 2^32 for the member. That is a format limit stated by the specification, not a result of a four-gigabyte execution experiment in this playbook. No 4-GiB test was performed or is necessary to establish the present 5-versus-15-byte counterexample.

Do not conflate the two layers:

RFC 1952:
    defines members and each member's CRC32/ISIZE trailer

GNU gzip:
    defines how its --list interface reports multi-member files

The format specification does not itself mandate GNU's exact --list presentation. Conversely, GNU's listing behavior does not change what RFC 1952 says a gzip member is.

Separate gzip integrity, expected completeness, and byte identity

Once the listing bug is exposed, replacing gzip --list with gzip -t alone would create a different faulty gate.

G4 proves why.

G4 consists only of B. It is a valid gzip stream. In the supplied preparation run, both the integrity test and full decode exited successfully:

candidate:       gzip(B)
gzip -t:         success
gzip -dc:        success
decoded length:  5
expected length: 15

Nothing is intrinsically corrupt about gzip(B). It is simply the wrong artifact for a consumer expecting A+B.

Its decision is therefore:

REJECT_CONTENT
not REJECT_DECODE.

That label preserves the distinction between a decoder failure and a semantically incomplete candidate.

G5 attacks the same assumption from the other direction. Its candidate is:

gzip(A) || gzip(B) || gzip(C)

All three members are intentionally valid. The supplied run reports successful integrity and decode operations, but the output is 20 bytes rather than the authorized 15.

The complete decoded bytes are A+B+C, whose SHA-256 is:

b698dd129ae2079cbd31d30888eea1f802f4d598d6a260329704e7245aa5eadb

Again:

REJECT_CONTENT

A gzip integrity test can tell the operator that a valid gzip stream was processed successfully. It cannot know that the consumer asked for exactly A+B rather than B, A+B+C, or some other valid content.

This is the completeness boundary:

gzip integrity:
    "Did this candidate satisfy the decoder's gzip checks?"

expected completeness:
    "Did the successful decode produce exactly 15 bytes?"

byte identity:
    "Are those 15 bytes the authorized A+B bytes?"

G3 demonstrates why even the second question is insufficient.

G3 reverses the members:

gzip(B) || gzip(A)

GNU gzip decodes concatenated members in sequence, so the result is B+A.

Its length is still:

5 + 10 = 15 bytes

Both gzip -t and gzip -dc succeeded in the supplied preparation run. A gate containing only these predicates would therefore approve it:

test_rc == 0
decode_rc == 0
decoded_length == 15

But the bytes are wrong.

A+B has:

0a8adbb8f207117540c252196eca37083eb708aa8668ed632740ed9c98f030d6

B+A has:

17e8b8d7c53463f5a19536e4f8c45649d88e624360fb52007a8eb38b5445da81

Therefore G3 is REJECT_CONTENT.

This is what a gzip member-order verification problem looks like when the acceptance contract is expressed at the decoded layer. The gate does not actually need to enumerate gzip members and enforce "A member first, B member second." It needs to establish that the resulting decoded byte sequence is A+B. Changing member order is merely one way to produce the wrong byte sequence.

That point is easy to miss because people often use "checksum" to mean several different things.

The CRC32 in a gzip trailer belongs to gzip's member-level data-integrity mechanism. RFC 1952 defines it as a CRC over the member's uncompressed data.

The SHA-256 in this admission contract belongs to a separate consumer expectation. Python's hashing interface can calculate that digest from decoded bytes, but the digest's authority comes from comparing it to a separately trusted expected value, not from the fact that SHA-256 was calculated successfully.

Neither should be substituted for the other.

For these tiny authored fixtures, the harness goes one step further after promotion and asserts:

assert published.read_bytes() == expected_payload

That direct equality check is useful in a diagnostic laboratory where the expected data is only a few bytes. The operational gate requested here remains successful decode/test plus expected decoded length and SHA-256. A production design may need a streaming comparison architecture rather than retaining an entire decoded object in memory.

Keep decoder failure authoritative and accept equivalent encodings only by contract

G6 and G7 cover a failure mode that is particularly dangerous in command-line batch systems: useful-looking stdout exists before the process has finished deciding whether the input was valid.

G6 starts with the valid concatenation A+B, then modifies one CRC byte in the final member's eight-byte trailer:

damaged = bytearray(a + b)
damaged[-8] ^= 1

RFC 1952 places CRC32 and ISIZE after the compressed blocks in each member trailer.

That ordering explains why an implementation can have emitted decoded data before it discovers a trailer failure. In the supplied preparation run, G6 produced 15 bytes on stdout but both gzip -t and gzip -dc returned status 1.

The disposition is unambiguous:

REJECT_DECODE

Do not convert it to REJECT_CONTENT merely because an output buffer exists. The decoder itself did not report successful completion.

G7 removes four bytes from the end of the otherwise valid A+B gzip stream:

(a + b)[:-4]

That damages the final trailer rather than intentionally deleting the member's compressed payload. The supplied preparation run again recorded 15 stdout bytes and status 1 from the integrity and decode commands.

Its decision is also:

REJECT_DECODE

A command-line workflow that validates only the bytes written to stdout can therefore make a false promotion. The sequence:

open destination
run decoder
observe 15 bytes
see expected-looking rows
promote destination

is incomplete.

The sequence has to include the final process outcome:

run integrity test
capture its exit status
run decode
capture the complete stdout candidate
capture the decode exit status
require both statuses to be zero
only then qualify length and digest
only then promote

The conceptual overlap with staging incomplete archive operations is limited to the handoff boundary: output can exist before an operation is qualified for publication. That neighboring page is about tar filesystem extraction; this gzip case produces one decoded byte stream and does not adopt tar member or extraction policy.

For the tiny diagnostic harness, subprocess.run(..., capture_output=True) intentionally keeps stdout in memory so the negative case is easy to inspect. That is not a recommendation for arbitrarily large or untrusted inputs. A real batch-ingestion service needs an explicit resource envelope (decoded-byte ceilings, disk-space policy, timeouts, and an appropriate streaming design) before its staging workflow can be considered qualified.

Under the decisions specified for this playbook, an otherwise plausible candidate with no approved resource envelope is HOLD. The correct response is not to pretend this 15-byte in-memory fixture has established production-scale safety.

The positive counterpart is G8.

Instead of:

gzip(A) || gzip(B)

G8 creates:

gzip(A+B)

one gzip member containing the complete 15-byte approved payload.

Its compressed bytes differ from the two-member representation. Its member count differs. Its member boundaries differ.

Its decoded bytes do not.

The supplied preparation run records:

listed uncompressed: 15
gzip -t:             success
gzip -dc:            success
decoded bytes:       15
decoded SHA-256:     expected A+B digest
decision:            ACCEPT_DECODED

That outcome is not an accidental loophole. It follows directly from the declared contract.

RFC 1952 permits a gzip file to be a series of members; the consumer's separate application contract decides whether member layout itself matters. Here it does not.

Therefore:

gzip(A) || gzip(B) -> ACCEPT_DECODED
gzip(A+B)           -> ACCEPT_DECODED

provided both successfully decode to exactly the authorized A+B bytes.

Do not use the decoded manifest later to claim that two source members were present. G8 exists precisely to disprove that inference. A decoded length and SHA-256 qualify decoded content; they do not prove member count, compressor provenance, per-member timestamps, or historical construction.

A system that genuinely requires those properties has a different acceptance contract and needs different evidence.

Reconcile every case and choose an explicit disposition

The eight fixtures form a small decision table rather than eight unrelated tricks.

The following ledger combines the supplied preparation observations with values directly determined from the authored fixture. For G6 and G7, the harness deliberately does not assert a --list value, because listing is not allowed to participate in admission for the damaged cases. Their decisive facts are the nonzero integrity and decoder statuses.

Case

Candidate meaning

--list uncompressed field

gzip -t

gzip -dc

Decoded bytes

Decoded SHA-256

Disposition

G1 single

A

10

0

0

10

ec3fbf048f04bbade045c58f187ad01d06c4a28700179888c2308bbaf29d6f2e

ACCEPT_DECODED

G2 concatenated

A+B as two members

5

0

0

15

0a8adbb8f207117540c252196eca37083eb708aa8668ed632740ed9c98f030d6

ACCEPT_DECODED; old counter is REPAIR_LIST_GATE

G3 reversed

B+A

10

0

0

15

17e8b8d7c53463f5a19536e4f8c45649d88e624360fb52007a8eb38b5445da81

REJECT_CONTENT

G4 member omitted

B only

5

0

0

5

bc6a3b29ef65267fedfa5d2f8fce98a611a11dcbb752e21727fdb6660bc05d23

REJECT_CONTENT

G5 member added

A+B+C

5

0

0

20

b698dd129ae2079cbd31d30888eea1f802f4d598d6a260329704e7245aa5eadb

REJECT_CONTENT

G6 CRC corrupt

A+B data with damaged final CRC

not acceptance-qualified

1

1

15 in supplied run

output exists but is not admissible after decoder failure

REJECT_DECODE

G7 truncated trailer

A+B with final trailer truncated

not acceptance-qualified

1

1

15 in supplied run

output exists but is not admissible after decoder failure

REJECT_DECODE

G8 one equivalent member

A+B as one member

15

0

0

15

0a8adbb8f207117540c252196eca37083eb708aa8668ed632740ed9c98f030d6

ACCEPT_DECODED


Several controls can now be stated without ambiguity.

gzip -t success is necessary in this playbook, but it is not artifact approval. G3, G4, and G5 all demonstrate valid compressed inputs that violate the requested decoded-content contract.

A size match is necessary but not sufficient. G3 produces the expected 15 bytes while putting those bytes in the wrong order.

A digest match is not enough if the decoder failed. G6 and G7 are classified from the nonzero processing status before potentially reassuring characteristics of stdout are considered.

A particular gzip member count is not necessary. G8 produces the same accepted decoded object as G2 through a different member layout.

The operational decision matrix should therefore be explicit:

Evidence state

Decision

Meaning

gzip -t == 0, gzip -dc == 0, decoded length equals trusted expected length, and decoded SHA-256 equals trusted expected SHA-256

ACCEPT_DECODED

Candidate satisfies the declared decoded-byte contract and may proceed to staged promotion

A validator uses gzip --list as though its multi-member uncompressed field were the complete decoded length

REPAIR_LIST_GATE

Validation control is measuring the wrong representation; candidate still requires ordinary artifact qualification

Integrity and decode succeed, but expected decoded length or SHA-256 differs

REJECT_CONTENT

Gzip object is processable but is not the authorized artifact

Integrity or decode returns nonzero, including cases where stdout looks complete

REJECT_DECODE

Decoder did not qualify the stream; do not publish its output

Trusted expectation is missing, or resource limits/staging assumptions are not qualified

HOLD

Evidence is insufficient to make an admission decision

Order the implementation accordingly. A practical gate is:

  1. Require a trusted expected decoded length and SHA-256.

  2. Require a qualified resource/staging envelope.

  3. Run gzip -t and retain its exit status.

  4. Decode into private staging and retain gzip -dc's final exit status.

  5. If either status is nonzero: REJECT_DECODE.

  6. Compare decoded length with the independent expectation.

  7. Compare decoded SHA-256 with the independent expectation.

  8. If either content comparison fails: REJECT_CONTENT.

  9. Otherwise: ACCEPT_DECODED.

  10. Promote only an ACCEPT_DECODED staged output.

gzip --list may still be useful diagnostically. It can tell an operator something about a gzip file. The problem is narrower: its multi-member uncompressed-size field must not be promoted into a whole-stream admission oracle when GNU documents that the value belongs to the last member.

This is a concrete form of the broader pipeline data-quality gates principle: successful processing and valid delivered data are separate states. The scope here remains one local gzip handoff, not a general ETL workflow.

Evidence retention should make that decision reproducible. At minimum, retain the candidate identity or controlled path, trusted-manifest identity, expected length and digest, validator source revision, source hash, executable/version records, integrity status, decode status, decoded byte count, decoded SHA-256, final disposition, and whether publication occurred.

Do not reduce that record to "PASS".

PASS_8_CASES in the diagnostic fixture means that all eight test expectations were satisfied. Some of those expectations require artifacts to be rejected. A passing negative-control test is evidence that the validator rejected an unsafe candidate correctly, not permission to admit that candidate.

Repair historical admissions and release only qualified staging

A batch system that previously treated gzip --list as the complete decoded-size oracle needs two separate recovery tracks.

First, repair the validator prospectively. Remove the assumption that the --list uncompressed field is the complete decoded length of a concatenated gzip stream. Preserve G2 as a regression control so a future refactor cannot silently restore that assumption.

Second, re-evaluate artifacts affected by the old gate from retained originals whenever the surrounding policy permits it.

Do not "repair" a candidate by rewriting its gzip member structure simply to make the listing look right. G2 does not need to be recompressed into one member to become semantically valid. It already decodes to the authorized A+B content. Recompressing it merely changes the representation to satisfy the broken measurement.

For artifacts that were previously rejected only because the old gate compared a multi-member --list value with a full-stream expected size, the recovery procedure is:

  • Retain the original compressed candidate.

  • Retain the historical decision evidence.

  • Apply the corrected decoder-status gate.

  • Decode into fresh private staging.

  • Measure the complete decoded output.

  • Compare length against the trusted historical expectation.

  • Compare SHA-256 against the trusted historical expectation.

  • Classify the candidate using the current decision matrix.

  • Publish only under the normal authorized handoff.

Do not silently invent an expectation after the fact. If a historical artifact has no trusted external expected length and digest, it cannot be rehabilitated merely because it decodes without an error. Its state is HOLD until the appropriate data owner supplies an authorized expectation through the system's established trust process.

Likewise, do not recover G6-style corruption by dropping the damaged member.

Suppose a damaged two-member input yields A successfully and then fails while processing B. Keeping A and declaring it the repaired artifact would change the contract from A+B to A.

That is not decoder recovery. It is content substitution.

A different payload may be perfectly useful, but accepting it requires separate authorization and a new expectation. This playbook explicitly excludes recovery by deleting corrupt members because the original admission question is whether the candidate produces the already approved decoded byte sequence.

Staging also needs a deliberately narrow interpretation.

The fixture writes all decoder stdout to:

staged.bin

inside an owned temporary directory. Only after all four admission predicates are true does it execute:

staged.replace(published)

On rejected cases, the assertion is:

assert not published.exists()

That establishes a useful local demonstration: unqualified bytes can exist in private staging without being exposed as the fixture's published output.

It does not establish a crash-durable publication protocol, cross-filesystem safety model, transaction boundary, or recovery guarantee after power loss. Those concerns are expressly outside this bounded experiment.

Operational ownership should preserve that boundary.

The ingestion process owns candidate creation and decode status. It must never translate "some stdout was written" into success.

The content owner or manifest authority owns the expected decoded length and SHA-256. The validator must not rewrite that expectation because a candidate nearly matches.

The release owner controls promotion from staging. It should receive the complete evidence ledger rather than a bare filename.

The operator responsible for the runtime environment owns the executable/version record, locale policy, resource limits, timeout policy, staging capacity, and changes to the validation implementation.

Version control is useful for the evidence-producing code. Versioning the validation procedure makes it easier to identify which validator source revision was deployed, but Git history cannot prove that the candidate bytes or manifest values were correct. The supplied source SHA-256 serves the similarly limited purpose of identifying this fixture's text; neither mechanism authenticates an execution by itself.

Rerun the negative controls when a material boundary changes: GNU gzip version, Python fixture runtime, command invocation, environment sanitization, expected-manifest format, digest procedure, staging mechanism, or promotion logic.

Resource-policy changes deserve the same discipline. This diagnostic uses capture_output=True, so decoder stdout is accumulated in memory. That design is appropriate for the authored 5-, 10-, 15-, and 20-byte examples. It should not be extrapolated into a production-scale memory budget.

A larger implementation can calculate a digest incrementally while streaming decoded bytes into a bounded staging object and maintain a running length. Python's hash objects support incremental update() operations, including the equivalence between hashing concatenated data and repeated updates. But the exact storage, maximum decoded size, disk quota, interruption behavior, and cleanup model belong to a separate production design review.

Until that resource envelope is established, use HOLD.

The reliability rule is intentionally conservative: qualification precedes promotion. A file appearing at a path is not qualification. Fifteen bytes appearing on stdout is not qualification. gzip -t returning zero is not qualification of business content. A SHA-256 computed from the candidate is not qualification unless a trusted independent digest exists to compare against.

Promotion is the result of the conjunction:

trusted expectation exists
AND resource/staging assumptions are qualified
AND gzip -t exits 0
AND gzip -dc exits 0
AND decoded length matches
AND decoded SHA-256 matches

Only then does the candidate become ACCEPT_DECODED.

Practice artifact verification through system administration

The durable lesson is not "never use gzip --list." It is to make each tool answer only the question its interface supports.

GNU gzip's listing is useful metadata. Its integrity test qualifies gzip processing. Full decompression establishes the candidate byte stream. An independently authored manifest establishes what the consumer expected. Length tests completeness at a coarse level. SHA-256 distinguishes equal-length but different decoded content. Private staging prevents unqualified output from becoming the handoff merely because a command created bytes.

That separation turns a confusing 5-versus-15 result into an auditable decision.

For administrators building the broader command-line, Linux, configuration, troubleshooting, security, and recovery skills around this kind of work, Refonte Learning's System Administration Program lists Windows and Linux systems, command-line work, system configuration, troubleshooting, security, backup and recovery among its curriculum areas, together with practical labs, projects, educational mentorship, and a potential internship. The program page also describes Training Certificate and Certificate of Internship awards on successful completion.

Those published curriculum areas provide relevant system-administration context; the page does not establish that this specific GNU gzip multi-member validation laboratory is taught.

For the gzip handoff itself, keep the acceptance boundary simpler than the compressed representation: successfully decode the whole candidate, qualify its complete length and SHA-256 against a trusted independent expectation, and promote only those staged bytes.