# `URP.Protocol`

Low-level URP (UNO Remote Protocol) binary wire format.

Handles framing, encoding, decoding, and reply parsing for the
binaryurp protocol used by LibreOffice's `soffice` process.

## References

  * [binaryurp source](https://git.libreoffice.org/core/+/refs/heads/master/binaryurp/)
  * [typeclass.h](https://git.libreoffice.org/core/+/refs/heads/master/include/typelib/typeclass.h)
  * [specialfunctionids.hxx](https://git.libreoffice.org/core/+/refs/heads/master/binaryurp/source/specialfunctionids.hxx)

# `dec_str`

```elixir
@spec dec_str(binary()) :: {binary(), binary()}
```

Decode a compressed string, returning `{string, rest}`.

# `enc_str`

```elixir
@spec enc_str(binary()) :: binary()
```

Encode a string with URP compressed-length prefix.

# `enc_str_iodata`

```elixir
@spec enc_str_iodata(binary()) :: iodata()
```

Like `enc_str/1` but returns iodata to avoid copying large payloads.

# `is_reply?`

```elixir
@spec is_reply?(binary()) :: boolean()
```

True if the frame is a reply (long header, no REQUEST flag).
Everything else (long-header request or short-header) is a request.

# `null_ctx`

```elixir
@spec null_ctx() :: binary()
```

Null CurrentContext reference: empty OID (0x00) + cache sentinel (0xFFFF).

# `one_way?`

```elixir
@spec one_way?(non_neg_integer()) :: boolean()
```

True if the given func_id is `release` (one-way, no reply expected).

Per the URP spec, `release` (func_id 2) is the only one-way call we'll
encounter from soffice. Sending a reply to a one-way call is a protocol
violation.

# `parse_any_string_reply`

```elixir
@spec parse_any_string_reply(binary()) :: {:ok, String.t()} | {:error, String.t()}
```

Parse a reply returning `any(string)` — extracts the string value.

# `parse_exception`

```elixir
@spec parse_exception(binary()) :: String.t() | nil
```

Extract a human-readable error message from an exception reply.

UNO exceptions are `Any` values containing a struct. The first member
of all exception structs is `Message` (string). Returns the message
string, or a fallback if parsing fails.

# `parse_int32_reply`

```elixir
@spec parse_int32_reply(binary()) :: {:ok, integer()} | {:error, String.t()}
```

Parse a reply returning a single signed 32-bit integer.

# `parse_interface_reply`

```elixir
@spec parse_interface_reply(binary()) :: {:ok, String.t()} | {:error, String.t()}
```

Parse a reply returning a single interface reference (OID string).

# `parse_qi_reply`

```elixir
@spec parse_qi_reply(binary()) :: {:ok, String.t()} | {:error, String.t()}
```

Parse a queryInterface reply — extracts OID from `any(XInterface)` return value.

# `parse_read_bytes_reply`

```elixir
@spec parse_read_bytes_reply(binary()) :: {:ok, binary()} | {:error, String.t()}
```

Parse a readBytes reply — return value (long) + out param (sequence<byte>).

# `parse_request`

```elixir
@spec parse_request(binary()) :: %{
  func_id: non_neg_integer(),
  body: binary(),
  type_cache: non_neg_integer() | nil,
  tid: binary() | nil
}
```

Parse an incoming request, extracting `func_id`, `body`, and `one_way` flag.

Handles both long headers (LONGHEADER set, REQUEST set) and short headers
(LONGHEADER not set — func_id in lower 6 bits, all cached values reused).

`one_way` is true when the sender does not expect a reply (MOREFLAGS absent
or MUSTREPLY not set). One-way calls like `release` must not receive replies.

# `parse_string_sequence_reply`

```elixir
@spec parse_string_sequence_reply(binary()) ::
  {:ok, [String.t()]} | {:error, String.t()}
```

Parse a reply returning `sequence<string>`.

# `property`

```elixir
@spec property(String.t(), non_neg_integer(), iodata()) :: iodata()
```

Encode a UNO PropertyValue struct.

# `recv_frame`

```elixir
@spec recv_frame(:gen_tcp.socket(), timeout(), pos_integer()) :: binary()
```

Receive a single URP block, returning the payload.

Raises if the block contains more than one message (the C++ writer always
sends count=1, but the spec allows count>1).

# `reply`

```elixir
@spec reply() :: binary()
```

Build a void reply (LONGHEADER only).

# `reply`

```elixir
@spec reply(binary()) :: binary()
```

Build a reply with body.

# `reply_with_tid`

```elixir
@spec reply_with_tid(binary()) :: binary()
```

Build a void reply with explicit TID (for cross-thread replies).

# `reply_with_tid`

```elixir
@spec reply_with_tid(binary(), binary()) :: binary()
```

Build a reply with body and explicit TID (for cross-thread replies).

# `request`

```elixir
@spec request(
  non_neg_integer(),
  keyword()
) :: binary()
```

Build a URP request header with automatic flag computation.

Options:
  * `:type` — `{:new, type_name, cache_idx}` or `{:cached, cache_idx}`
  * `:oid`  — `{oid_string, cache_idx}`
  * `:tid`  — `{tid_bytes, cache_idx}`

Omitting an option reuses the value from the previous message on the wire.

# `send_frame`

```elixir
@spec send_frame(:gen_tcp.socket(), iodata()) :: :ok
```

Send a single URP block: `<<size::32, count::32, payload>>`.

# `type_cached`

```elixir
@spec type_cached(non_neg_integer()) :: binary()
```

Reference a type already in the peer's cache.

# `type_new`

```elixir
@spec type_new(String.t(), non_neg_integer()) :: iodata()
```

Register a new interface type in the peer's cache.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
