defmodule Phex do # Phex - Elixir PHP serialized decoder and encoder. # Copyright (C) 2019 Hans Bernhard Goedeke # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. @moduledoc ~S""" A PHP serialized decoder and encoder. ## Installation The package can be installed by adding `phex` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:phex, "~> 0.1.0"} ] end ``` ## Basic Usage iex> Phex.encode!(%{"age" => 42, "name" => "John Doe"}) "a:2:{s:3:\"age\";i:42;s:4:\"name\";s:8:\"John Doe\";}" iex> Phex.decode!("a:2:{s:3:\"age\";i:42;s:4:\"name\";s:8:\"John Doe\";}") %{"age" => 42, "name" => "John Doe"} ## PHP Serialization Currently only maps with keys which are either integers or strings are supported. This works in most cases but can create invalid serialized data if the peculiarities of PHP arrays are not taken into account. More information can be found in the PHP manual [here](https://www.php.net/manual/en/language.types.array.php). As of now supported types are ... iex> encode!(nil) "N;" iex> decode!("N;") nil ### Boolean iex> encode!(true) "b:1;" iex> decode!("b:0;") false ### Integer iex> encode!(9000) "i:9000;" iex> decode!("i:-212;") -212 ### Float iex> encode!(3.14159265359) "d:3.14159265359;" iex> decode!("d:6.62607004;") 6.62607004 ### String iex> encode!("Hello") "s:5:\"Hello\";" iex> decode!("s:5:\"World\";") "World" ### Maps (PHP-Arrays) iex> encode!(%{"Blue" => 2, "Pink" => 4}) "a:2:{s:4:\"Blue\";i:2;s:4:\"Pink\";i:4;}" iex> decode!("a:2:{s:6:\"Bunker\";s:4:\"Blue\";s:7:\"Buscemi\";s:4:\"Pink\";}") %{"Bunker" => "Blue", "Buscemi" => "Pink"} ### Objects iex> decode!(~S(O:8:"PhpClass":2:{s:21:"PhpClassaPrivateVar";s:16:"A private String";s:10:"aPublicVar";s:15:"A public String";})) %{ :__object__ => "PhpClass", "PhpClassaPrivateVar" => "A private String", "aPublicVar" => "A public String" } """ def decode!(binary) when is_binary(binary) do case decode(binary) do {:ok, result, _rest} -> result {:error, reason, _rest} -> raise reason end end def decode(binary) def decode(<<"N;", rest::binary>>) do {:ok, nil, rest} end def decode(<<"b:0;", rest::binary>>) do {:ok, false, rest} end def decode(<<"b:1;", rest::binary>>) do {:ok, true, rest} end def decode(<<"b:", rest::binary>>) do {:error, "can't parse boolean", rest} end def decode(<<"i:", rest::binary>>) do case Integer.parse(rest) do {value, <<";", rest::binary>>} -> {:ok, value, rest} {_val, rest} -> {:error, "integer missing terminator", rest} :error -> {:error, "can't parse integer", rest} end end def decode(<<"d:", rest::binary>>) do case Float.parse(rest) do {value, <<";", rest::binary>>} -> {:ok, value, rest} {_val, rest} -> {:error, "float missing terminator", rest} :error -> {:error, "can't parse float", rest} end end def decode(<<"s:", _rest::binary>> = rest) do decode_string(rest) end def decode(<<"a:", rest::binary>>) do with {size, <<":", rest::binary>>} <- Integer.parse(rest), {:ok, value, rest} <- decode_array(size, rest) do {:ok, value, rest} else {:error, _msg, _rest} = error -> error {_size, rest} -> {:error, "can't parse array - missing size", rest} end end def decode(<<"O:", rest::binary>>) do with {size, <<":", rest::binary>>} <- Integer.parse(rest), {:ok, name, <<":", rest::binary>>} <- decode_string(size, rest, 0), {size, <<":", rest::binary>>} <- Integer.parse(rest), {:ok, values, rest} <- decode_object_values(size, rest) do {:ok, Map.put(values, :__object__, name), rest} else {:error, _msg, _rest} = error -> error {_size, rest} -> {:error, "can't parse object - missing size", rest} end end def decode(rest) when is_binary(rest) do {:error, "unable to decode", rest} end defp decode_string(binary, offset \\ 0) defp decode_string(<<"s:", rest::binary>>, offset) do with {size, <<":", rest::binary>>} <- Integer.parse(rest), {:ok, value, <<";", rest::binary>>} <- decode_string(size, rest, offset) do {:ok, value, rest} end end defp decode_string(size, binary, offset) do case binary do <<"\"", rest::binary>> -> decode_string("", size, rest, offset) rest -> {:error, "String missing starting delimiter", rest} end end defp decode_string(acc, size, rest, offset) when offset < 0 do decode_string(acc, size + offset, rest, abs(offset)) end defp decode_string(acc, size, <<"\"", rest::binary>>, offset) do if size == 0 or size + offset == 0 do {:ok, acc, rest} else {:error, "string length incorrect", rest} end end defp decode_string(acc, size, <<"\\", rest::binary>>, offset) when size > 0 do rest |> case do <<"n", rest::binary>> -> {:ok, "\n", rest} <<"r", rest::binary>> -> {:ok, "\r", rest} <<"t", rest::binary>> -> {:ok, "\t", rest} <<"v", rest::binary>> -> {:ok, "\v", rest} <<"e", rest::binary>> -> {:ok, "\e", rest} <<"f", rest::binary>> -> {:ok, "\f", rest} <<"\\", rest::binary>> -> {:ok, "\\", rest} <<"$", rest::binary>> -> {:ok, "$", rest} <<"\"", rest::binary>> -> {:ok, "\"", rest} <> -> {:error, "unknown escaped char", rest} end |> case do {:ok, unescaped, rest} -> decode_string(acc <> unescaped, size - 1, rest, offset) error -> error end end defp decode_string(acc, size, <>, offset) when size > 0 do decode_string(<>, size - byte_size(<>), rest, offset) end defp decode_string(acc, size, rest, offset) do cond do size < 0 and offset == 0 -> {:error, "incorrect string length", rest} size <= 0 and offset > 0 -> decode_string(acc, offset, rest, 0) true -> {:error, "can't parse string", rest} end end defp decode_array(size, rest) defp decode_array(size, <<"{", rest::binary>>) do case decode_array(size, rest) do {:ok, array, rest} -> {:ok, Map.new(array), rest} error -> error end end defp decode_array(size, <<"}", rest::binary>>) do if size == 0 do {:ok, [], rest} else {:error, "incorrect array length", rest} end end defp decode_array(size, rest) do with {:ok, key, rest} <- decode(rest), {:ok, value, rest} <- decode(rest), {:ok, array, rest} <- decode_array(size - 1, rest) do {:ok, [{key, value} | array], rest} end end defp decode_object_values(size, rest) defp decode_object_values(size, <<"{", rest::binary>>) do case decode_object_values(size, rest) do {:ok, array, rest} -> {:ok, Map.new(array), rest} error -> error end end defp decode_object_values(size, <<"}", rest::binary>>) do if size == 0 do {:ok, [], rest} else {:error, "incorrect array length", rest} end end defp decode_object_values(size, rest) do with {:ok, key, rest} <- decode_string(rest, -2), {:ok, value, rest} <- decode(rest), {:ok, array, rest} <- decode_object_values(size - 1, rest) do {:ok, [{key, value} | array], rest} end end def encode!(term) do case encode(term) do {:ok, result} -> result {:error, reason, _rest} -> raise reason end end def encode(term) def encode(nil) do {:ok, "N;"} end def encode(false) do {:ok, "b:0;"} end def encode(true) do {:ok, "b:1;"} end def encode(number) when is_integer(number) do {:ok, "i:" <> Integer.to_string(number) <> ";"} end def encode(number) when is_float(number) do {:ok, "d:" <> Float.to_string(number) <> ";"} end def encode(binary) when is_binary(binary) do {:ok, "s:" <> Integer.to_string(byte_size(binary)) <> ":\"" <> encode_string(binary) <> "\";"} end def encode(map) when is_map(map) do map |> Map.to_list() |> encode_array() end defp encode_string(acc \\ <<>>, binary) defp encode_string(acc, <<>>) do acc end defp encode_string(acc, rest) when is_binary(rest) do rest |> case do <<"\n", rest::binary>> -> {:ok, "\\n", rest} <<"\r", rest::binary>> -> {:ok, "\\r", rest} <<"\t", rest::binary>> -> {:ok, "\\t", rest} <<"\v", rest::binary>> -> {:ok, "\\v", rest} <<"\e", rest::binary>> -> {:ok, "\\e", rest} <<"\f", rest::binary>> -> {:ok, "\\f", rest} <<"\\", rest::binary>> -> {:ok, "\\\\", rest} <<"$", rest::binary>> -> {:ok, "\\$", rest} <<"\"", rest::binary>> -> {:ok, "\\\"", rest} <> -> {:ok, <>, rest} rest -> {:error, "invalid character", rest} end |> case do {:ok, escaped, rest} -> encode_string(acc <> escaped, rest) error -> error end end defp encode_array(rest, acc \\ "") defp encode_array(rest, "") do encode_array(rest, "a:" <> Integer.to_string(length(rest)) <> ":{") end defp encode_array([{key, value} | rest], acc) when is_binary(key) or is_integer(key) do with {:ok, encoded_key} <- encode(key), {:ok, encoded_value} <- encode(value) do encode_array(rest, acc <> encoded_key <> encoded_value) end end defp encode_array([], acc) when acc != "" do {:ok, acc <> "}"} end defp encode_array(list, _acc) do {:error, "array encode", list} end end