Skip to content

decode rejects header-only stream (valid empty Snappy frame) #7

@GrapeBaBa

Description

@GrapeBaBa

Bug

snappyframesz.decode rejects a stream that contains only the 10-byte stream-identifier chunk (no data chunks), even though the Snappy framing spec makes this a valid representation of an empty payload.

Repro

const std = @import("std");
const frames = @import("snappyframesz");

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    // Stream identifier chunk only — no data chunks.
    // ff 06 00 00 sNaPpY  (10 bytes)
    const header_only = "\xff\x06\x00\x00sNaPpY";

    // Sanity: encoding the empty payload through the same lib produces
    // additional chunks because finish() forces an empty data chunk
    // when nothing was written.
    const encoded_via_lib = try frames.encode(allocator, "");
    defer allocator.free(encoded_via_lib);
    std.debug.print("lib encode(\"\").len = {d}\n", .{encoded_via_lib.len});

    // The header-only form is what every other Snappy implementation
    // accepts (Go `snappy.NewReader`, Rust `snap`). snappyframesz
    // returns FrameError.NotFramed instead of an empty slice.
    const decoded = try frames.decode(allocator, header_only);
    defer allocator.free(decoded);
    std.debug.print("decode(header_only).len = {d}\n", .{decoded.len});
}

Actual: error: NotFramed
Expected: [] (empty slice)

Root cause

src/frames.zig:276 (current master):

```zig
if (!saw_data_chunk) return FrameError.NotFramed;
```

The function exits successfully only after at least one data chunk has been seen. A stream that contains only the stream identifier — which is a perfectly valid empty Snappy frame — falls through this branch and returns NotFramed. The outer decode then falls back to snappyz.decode on the magic-header bytes, which also fails.

Cross-impl behaviour

Same 10-byte input `ff 06 00 00 sNaPpY`:

Impl Result
Go `snappy.NewReader` OK, `[]byte{}`
Rust `snap::read::FrameDecoder` OK, `b""`
Python `cramjam` OK
snappyframesz (this lib) `FrameError.NotFramed`

This bites cross-client interop testing — leanSpec emits exactly this 10-byte stream as the canonical empty-input fixture for `snappy_frame`, and the zeam spec-test runner currently has to skip that one fixture explicitly.

Suggested fix

Drop the `saw_data_chunk` guard, or make it conditional on having seen neither a stream identifier nor a data chunk:

```zig
if (!saw_stream_identifier and !saw_data_chunk) return FrameError.NotFramed;
```

A stream identifier alone is a complete (empty) frame; the function should return an empty slice in that case.

Discovered via

zeam PR #715 — implementing the leanSpec `networking_codec` spec-test runner. The empty-input snappy_frame fixture (`leanSpec/fixtures/consensus/networking_codec/devnet/networking/test_snappy_codec/test_snappy_frame_empty.json`, expectedFramed=`0xff060000734e61507059`) is the only one that doesn't round-trip through `snappyframesz`.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions