EncodingRs (encoding_rs v0.3.0)

Copy Markdown

High-performance string encoding/decoding using Rust's encoding_rs crate.

This library provides fast character encoding conversion using the same encoding library that powers Firefox. It decodes all encodings in the WHATWG Encoding Standard and encodes their output encodings.

Features

  • High performance: Uses encoding_rs, the same library used by Firefox
  • Dirty schedulers: Large binaries automatically use dirty CPU schedulers to avoid blocking the BEAM (default threshold: 64KB)
  • Safe error handling: Returns {:ok, result} or {:error, reason} tuples
  • WHATWG compliant: Implements the WHATWG decoding and encoding algorithms

Operation options

Encoding, decoding, batch, and streaming functions accept per-call options:

  • :dirty_threshold — byte size above which dirty CPU schedulers are used (default: 65,536).

  • :max_input_size — maximum input or chunk size (default: 104,857,600). Set it to :infinity only for trusted, externally bounded input.

    EncodingRs.decode(data, "shift_jis",

    dirty_threshold: 128 * 1024,
    max_input_size: 10 * 1024 * 1024

    )

Warning

Disabling the size limit or setting it very high removes a safety guardrail against memory exhaustion. Only do this when inputs are trusted and bounded by other means (e.g., request body limits, file size checks). For untrusted input, prefer the streaming decoder (EncodingRs.Decoder) with bounded chunk sizes.

New code should prefer explicit options. Existing application configuration remains supported as a compatibility fallback.

Supported Encodings

  • UTF-8 (encode/decode); UTF-16LE and UTF-16BE (decode only)
  • Windows code pages: 874, 1250-1258, 949, 932
  • ISO-8859 family: 2, 3, 4, 5, 6, 7, 8, 8-I, 10, 13, 14, 15, 16
  • IBM866
  • KOI8-R, KOI8-U
  • macintosh, x-mac-cyrillic
  • Asian encodings: Shift_JIS, EUC-JP, ISO-2022-JP, EUC-KR, GBK, GB18030, Big5
  • x-user-defined

Examples

iex> EncodingRs.encode("Hello", "windows-1252")
{:ok, "Hello"}

iex> EncodingRs.decode(<<72, 101, 108, 108, 111>>, "windows-1252")
{:ok, "Hello"}

iex> EncodingRs.encode!("¥₪ש", "windows-1255")
<<165, 164, 249>>

iex> EncodingRs.decode!(<<165, 164, 249>>, "windows-1255")
"¥₪ש"

iex> EncodingRs.encoding_exists?("utf-8")
true

iex> EncodingRs.encoding_exists?("not-an-encoding")
false

Summary

Types

Result from batch operations

Result of BOM detection: encoding name and BOM length in bytes.

Input item for batch decoding: {binary, encoding}

Detailed one-shot decode result, including BOM selection and replacements.

Input item for batch encoding: {string, encoding}

An encoding label string (e.g., "utf-8", "shift_jis", "windows-1252").

Error reason atoms returned by encoding/decoding functions.

Per-operation scheduler and input-size options.

Functions

Returns whether the native implementation is loaded and callable.

Returns the canonical name for an encoding label.

Decodes a binary from the specified encoding to a UTF-8 string.

Decodes a binary from the specified encoding to a UTF-8 string.

Decodes multiple binaries in a single NIF call.

Decodes multiple binaries while reporting each actual encoding and whether malformed input was replaced.

Decodes a complete binary and reports the encoding actually used and whether malformed input was replaced with U+FFFD.

Detects the encoding from a BOM and strips it from the data.

Detects the encoding from a Byte Order Mark (BOM) at the start of the data.

Returns the threshold (in bytes) above which dirty schedulers are used.

Encodes a UTF-8 string to the specified encoding.

Encodes a UTF-8 string to the specified encoding.

Encodes multiple strings in a single NIF call.

Checks if an encoding label is recognized for decoding.

Returns all recognized encoding names, including decode-only encodings.

Returns the maximum input size (in bytes) allowed for encoding/decoding operations.

Types

batch_result(t)

@type batch_result(t) ::
  {:ok, t}
  | {:error, :unknown_encoding | :encoder_unavailable | :input_too_large}

Result from batch operations

bom_result()

@type bom_result() ::
  {:ok, encoding(), bom_length :: non_neg_integer()} | {:error, :no_bom}

Result of BOM detection: encoding name and BOM length in bytes.

decode_batch_item()

@type decode_batch_item() :: {binary(), encoding()}

Input item for batch decoding: {binary, encoding}

decode_details_result()

@type decode_details_result() ::
  {:ok, String.t(), actual_encoding :: encoding(), had_errors :: boolean()}
  | {:error, :unknown_encoding | :input_too_large}

Detailed one-shot decode result, including BOM selection and replacements.

encode_batch_item()

@type encode_batch_item() :: {String.t(), encoding()}

Input item for batch encoding: {string, encoding}

encoding()

@type encoding() :: String.t()

An encoding label string (e.g., "utf-8", "shift_jis", "windows-1252").

See list_encodings/0 for all recognized encodings, or check the WHATWG Encoding Standard.

error_reason()

@type error_reason() ::
  :unknown_encoding | :encoder_unavailable | :no_bom | :input_too_large

Error reason atoms returned by encoding/decoding functions.

options()

@type options() :: [
  dirty_threshold: non_neg_integer(),
  max_input_size: non_neg_integer() | :infinity
]

Per-operation scheduler and input-size options.

Functions

available?()

@spec available?() :: boolean()

Returns whether the native implementation is loaded and callable.

This check never raises when the NIF is unavailable, allowing applications to disable optional encoding features cleanly.

Examples

iex> EncodingRs.available?()
true

canonical_name(encoding)

@spec canonical_name(encoding()) :: {:ok, encoding()} | {:error, :unknown_encoding}

Returns the canonical name for an encoding label.

Encoding labels have many aliases (e.g., "latin1", "iso-8859-1", "iso_8859-1"). This function returns the canonical WHATWG name for any valid alias.

Examples

iex> EncodingRs.canonical_name("latin1")
{:ok, "windows-1252"}

iex> EncodingRs.canonical_name("utf8")
{:ok, "UTF-8"}

iex> EncodingRs.canonical_name("invalid")
{:error, :unknown_encoding}

decode(binary, encoding)

@spec decode(binary(), encoding()) ::
  {:ok, String.t()} | {:error, :unknown_encoding | :input_too_large}

Decodes a binary from the specified encoding to a UTF-8 string.

Returns {:ok, string} on success, or {:error, reason} on failure. Malformed byte sequences are replaced with the Unicode replacement character (U+FFFD). An input BOM may override the requested encoding. Use decode_with_details/3 when either behavior must be observed.

Automatically uses dirty CPU schedulers for binaries larger than the selected threshold (see dirty_threshold/1).

Examples

iex> EncodingRs.decode(<<72, 101, 108, 108, 111>>, "windows-1252")
{:ok, "Hello"}

iex> EncodingRs.decode(<<0xFF>>, "invalid-encoding")
{:error, :unknown_encoding}

decode(binary, encoding, opts)

@spec decode(binary(), encoding(), options()) ::
  {:ok, String.t()} | {:error, :unknown_encoding | :input_too_large}

decode!(binary, encoding)

@spec decode!(binary(), encoding()) :: String.t()

Decodes a binary from the specified encoding to a UTF-8 string.

Returns the decoded string on success, or raises an ArgumentError on failure.

Examples

iex> EncodingRs.decode!(<<72, 101, 108, 108, 111>>, "windows-1252")
"Hello"

iex> EncodingRs.decode!(<<0xFF>>, "invalid-encoding")
** (ArgumentError) unknown encoding: invalid-encoding

decode!(binary, encoding, opts)

@spec decode!(binary(), encoding(), options()) :: String.t()

decode_batch(items)

@spec decode_batch([decode_batch_item()]) :: [batch_result(String.t())]

Decodes multiple binaries in a single NIF call.

This is more efficient than calling decode/2 repeatedly when processing many items, as it amortizes the NIF dispatch overhead.

Results are returned in the same order as the input items.

The combined byte size determines whether the batch uses a normal or dirty CPU scheduler. See the Batch Processing Guide for details.

Arguments

  • items - List of {binary, encoding} tuples to decode

Returns

List of {:ok, string}, {:error, :unknown_encoding}, or {:error, :input_too_large} tuples.

Examples

iex> items = [{<<72, 101, 108, 108, 111>>, "windows-1252"}, {<<0x82, 0xA0>>, "shift_jis"}]
iex> EncodingRs.decode_batch(items)
[{:ok, "Hello"}, {:ok, "あ"}]

iex> EncodingRs.decode_batch([{<<72>>, "invalid-encoding"}])
[{:error, :unknown_encoding}]

decode_batch(items, opts)

@spec decode_batch([decode_batch_item()], options()) :: [batch_result(String.t())]

decode_batch_with_details(items)

@spec decode_batch_with_details([decode_batch_item()]) :: [decode_details_result()]

Decodes multiple binaries while reporting each actual encoding and whether malformed input was replaced.

Results have the same order as the inputs. See decode_with_details/3 for the detailed success tuple.

Examples

iex> EncodingRs.decode_batch_with_details([{"hello", "utf-8"}, {<<0xFF>>, "utf-8"}])
[{:ok, "hello", "UTF-8", false}, {:ok, "�", "UTF-8", true}]

decode_batch_with_details(items, opts)

@spec decode_batch_with_details([decode_batch_item()], options()) :: [
  decode_details_result()
]

decode_with_details(binary, encoding)

@spec decode_with_details(binary(), encoding()) :: decode_details_result()

Decodes a complete binary and reports the encoding actually used and whether malformed input was replaced with U+FFFD.

The actual encoding can differ from the requested label when the input starts with a UTF-8 or UTF-16 BOM.

Examples

iex> EncodingRs.decode_with_details(<<0xFF>>, "utf-8")
{:ok, "�", "UTF-8", true}

iex> EncodingRs.decode_with_details(<<0xFF, 0xFE, 0x48, 0x00>>, "windows-1252")
{:ok, "H", "UTF-16LE", false}

decode_with_details(binary, encoding, opts)

@spec decode_with_details(binary(), encoding(), options()) :: decode_details_result()

detect_and_strip_bom(data)

@spec detect_and_strip_bom(binary()) ::
  {:ok, encoding(), binary()} | {:error, :no_bom}

Detects the encoding from a BOM and strips it from the data.

Convenience function that combines BOM detection with stripping the BOM from the input data. Useful when you want to both detect the encoding and get the data without the BOM prefix.

Returns

  • {:ok, encoding, data_without_bom} - BOM detected and stripped
  • {:error, :no_bom} - No BOM found, data unchanged

Examples

iex> EncodingRs.detect_and_strip_bom(<<0xEF, 0xBB, 0xBF, "hello">>)
{:ok, "UTF-8", "hello"}

iex> EncodingRs.detect_and_strip_bom("hello")
{:error, :no_bom}

detect_bom(data)

@spec detect_bom(binary()) :: bom_result()

Detects the encoding from a Byte Order Mark (BOM) at the start of the data.

BOMs are special byte sequences at the beginning of a file that indicate the encoding. This function checks the first few bytes of the input and returns the detected encoding if a BOM is found.

Supported BOMs:

  • UTF-8: <<0xEF, 0xBB, 0xBF>> (3 bytes)
  • UTF-16LE: <<0xFF, 0xFE>> (2 bytes)
  • UTF-16BE: <<0xFE, 0xFF>> (2 bytes)

Returns

  • {:ok, encoding, bom_length} - BOM detected, returns encoding name and BOM size
  • {:error, :no_bom} - No BOM found at the start of the data

Examples

iex> EncodingRs.detect_bom(<<0xEF, 0xBB, 0xBF, "hello">>)
{:ok, "UTF-8", 3}

iex> EncodingRs.detect_bom(<<0xFF, 0xFE, 0x48, 0x00>>)
{:ok, "UTF-16LE", 2}

iex> EncodingRs.detect_bom(<<0xFE, 0xFF, 0x00, 0x48>>)
{:ok, "UTF-16BE", 2}

iex> EncodingRs.detect_bom("hello")
{:error, :no_bom}

iex> EncodingRs.detect_bom(<<>>)
{:error, :no_bom}

dirty_threshold()

@spec dirty_threshold() :: non_neg_integer()

Returns the threshold (in bytes) above which dirty schedulers are used.

Encode/decode operations on binaries larger than this threshold will automatically use dirty CPU schedulers to avoid blocking the BEAM's normal schedulers. This prevents long-running encoding operations from causing latency for other processes.

Pass :dirty_threshold to an operation to override the 64KB default for that call. The value must be a non-negative integer.

Examples

iex> EncodingRs.dirty_threshold()
65536

dirty_threshold(opts)

@spec dirty_threshold(options()) :: non_neg_integer()

encode(string, encoding)

@spec encode(String.t(), encoding()) ::
  {:ok, binary()}
  | {:error, :unknown_encoding | :encoder_unavailable | :input_too_large}

Encodes a UTF-8 string to the specified encoding.

Returns {:ok, binary} on success, or {:error, reason} on failure. Unmappable characters are replaced with a suitable fallback character.

UTF-16LE and UTF-16BE are decode-only in the WHATWG Encoding Standard. Those labels and replacement return {:error, :encoder_unavailable}; this function never returns bytes in a different encoding than requested.

Automatically uses dirty CPU schedulers for strings larger than the selected threshold (see dirty_threshold/1).

Examples

iex> EncodingRs.encode("Hello", "windows-1252")
{:ok, "Hello"}

iex> EncodingRs.encode("Hello", "invalid-encoding")
{:error, :unknown_encoding}

encode(string, encoding, opts)

@spec encode(String.t(), encoding(), options()) ::
  {:ok, binary()}
  | {:error, :unknown_encoding | :encoder_unavailable | :input_too_large}

encode!(string, encoding)

@spec encode!(String.t(), encoding()) :: binary()

Encodes a UTF-8 string to the specified encoding.

Returns the encoded binary on success, or raises an ArgumentError on failure.

Examples

iex> EncodingRs.encode!("Hello", "windows-1252")
"Hello"

iex> EncodingRs.encode!("Hello", "invalid-encoding")
** (ArgumentError) unknown encoding: invalid-encoding

encode!(string, encoding, opts)

@spec encode!(String.t(), encoding(), options()) :: binary()

encode_batch(items)

@spec encode_batch([encode_batch_item()]) :: [batch_result(binary())]

Encodes multiple strings in a single NIF call.

This is more efficient than calling encode/2 repeatedly when processing many items, as it amortizes the NIF dispatch overhead.

Results are returned in the same order as the input items.

The combined byte size determines whether the batch uses a normal or dirty CPU scheduler. See the Batch Processing Guide for details.

Arguments

  • items - List of {string, encoding} tuples to encode

Returns

List of {:ok, binary}, {:error, :unknown_encoding}, {:error, :encoder_unavailable}, or {:error, :input_too_large} tuples.

Examples

iex> items = [{"Hello", "windows-1252"}, {"あ", "shift_jis"}]
iex> EncodingRs.encode_batch(items)
[{:ok, "Hello"}, {:ok, <<130, 160>>}]

iex> EncodingRs.encode_batch([{"test", "invalid-encoding"}])
[{:error, :unknown_encoding}]

encode_batch(items, opts)

@spec encode_batch([encode_batch_item()], options()) :: [batch_result(binary())]

encoding_exists?(encoding)

@spec encoding_exists?(encoding()) :: boolean()

Checks if an encoding label is recognized for decoding.

This returns true for decode-only labels such as UTF-16LE and UTF-16BE.

Examples

iex> EncodingRs.encoding_exists?("utf-8")
true

iex> EncodingRs.encoding_exists?("UTF-8")
true

iex> EncodingRs.encoding_exists?("not-an-encoding")
false

list_encodings()

@spec list_encodings() :: [encoding()]

Returns all recognized encoding names, including decode-only encodings.

Examples

iex> "UTF-8" in EncodingRs.list_encodings()
true

iex> "Shift_JIS" in EncodingRs.list_encodings()
true

max_input_size()

@spec max_input_size() :: non_neg_integer() | :infinity

Returns the maximum input size (in bytes) allowed for encoding/decoding operations.

Inputs larger than this limit will return {:error, :input_too_large} instead of being passed to the NIF. This prevents excessive memory allocation from untrusted or unexpectedly large inputs.

Pass :max_input_size to an operation to override the 100MB default for that call.

Set to :infinity to disable the size limit entirely. This is appropriate for trusted environments where inputs are known to be safe, but should be avoided when processing untrusted data — a large input can cause memory amplification of up to 3x in the NIF (input buffer + output buffer + BEAM binary copy).

EncodingRs.decode(data, "utf-8", max_input_size: :infinity)

The value must be a non-negative integer or :infinity. Invalid values (e.g., strings, negative numbers) will raise an ArgumentError on first use.

Examples

iex> EncodingRs.max_input_size()
104857600

max_input_size(opts)

@spec max_input_size(options()) :: non_neg_integer() | :infinity