Skip to main content

src/nhttp_str.erl

-module(nhttp_str).

-moduledoc """
String literal codec for HPACK and QPACK.

Implements the string literal representation shared by HPACK (RFC 7541
Section 5.2) and QPACK (RFC 9204 Section 4.1.2). A string literal is
encoded as:

    H (1 bit) | Length (7-bit prefixed integer) | Data (Length bytes)

When H=1 the data is Huffman-coded using the static table from
RFC 7541 Appendix B. When H=0 the data is the raw octets.
""".

%%%-----------------------------------------------------------------------------
%% EXPORTS
%%%-----------------------------------------------------------------------------
-export([decode/1]).

-export([encode/2]).

%%%-----------------------------------------------------------------------------
%% TYPE EXPORTS
%%%-----------------------------------------------------------------------------
-export_type([decode_error/0]).

%%%-----------------------------------------------------------------------------
%% TYPES
%%%-----------------------------------------------------------------------------
-type decode_error() :: incomplete | invalid_huffman.

%%%-----------------------------------------------------------------------------
%% DECODING
%%%-----------------------------------------------------------------------------
-doc """
Decode a string literal from a bitstring.

Reads the H-bit, decodes the 7-bit-prefixed length, extracts that many
bytes, and optionally Huffman-decodes the result. Returns the decoded
string and the unconsumed remainder.
""".
-spec decode(bitstring()) ->
    {ok, binary(), bitstring()} | {error, decode_error()}.
decode(<<>>) ->
    {error, incomplete};
decode(<<H:1, Rest0/bits>>) ->
    maybe
        {ok, Length, Rest1} ?= dec7_map_error(Rest0),
        case Rest1 of
            <<Data:Length/binary, Rest/bits>> ->
                decode_data(H, Data, Rest);
            _ ->
                {error, incomplete}
        end
    end;
decode(_) ->
    {error, incomplete}.

%%%-----------------------------------------------------------------------------
%% ENCODING
%%%-----------------------------------------------------------------------------
-doc """
Encode a binary as a string literal.
When `Huffman` is `true`, the string is Huffman-encoded and the H-bit
is set. When `false`, the raw bytes are written with H=0.
""".
-spec encode(binary(), boolean()) -> iolist().
encode(Str, true) ->
    Encoded = nhttp_huffman:encode(Str),
    [nhttp_int:enc7(byte_size(Encoded), 2#1), Encoded];
encode(Str, false) ->
    [nhttp_int:enc7(byte_size(Str), 2#0), Str].

%%%-----------------------------------------------------------------------------
%% INTERNAL FUNCTIONS
%%%-----------------------------------------------------------------------------
-spec dec7_map_error(bitstring()) ->
    {ok, non_neg_integer(), bitstring()} | {error, decode_error()}.
dec7_map_error(Bits) ->
    case nhttp_int:dec7(Bits) of
        {ok, _, _} = Ok -> Ok;
        {error, incomplete} -> {error, incomplete};
        {error, overflow} -> {error, incomplete}
    end.

-spec decode_data(0 | 1, binary(), bitstring()) ->
    {ok, binary(), bitstring()} | {error, invalid_huffman}.
decode_data(0, Data, Rest) ->
    {ok, Data, Rest};
decode_data(1, Data, Rest) ->
    case nhttp_huffman:decode(Data) of
        {ok, Decoded} -> {ok, Decoded, Rest};
        {error, _} -> {error, invalid_huffman}
    end.