Current section
Files
Jump to
Current section
Files
lib/pdf_ex/cos/lexer.ex
defmodule PdfEx.COS.Lexer do
@moduledoc false
@type token ::
{:name, binary()}
| {:literal_string, binary()}
| {:hex_string, binary()}
| {:number, number()}
| {:boolean, boolean()}
| :null
| :dict_open
| :dict_close
| :array_open
| :array_close
| {:keyword, atom() | binary()}
@type result :: {:ok, token(), non_neg_integer()} | {:error, atom()} | :eof
@whitespace [?\s, ?\t, ?\r, ?\n, ?\0, ?\f]
@delimiters [?(, ?), ?<, ?>, ?[, ?], ?{, ?}, ?/, ?%]
@spec tokenize_next(binary(), integer()) :: result()
def tokenize_next(bin, offset) when offset < 0 or offset >= byte_size(bin), do: :eof
def tokenize_next(bin, offset) do
<<char>> = binary_part(bin, offset, 1)
cond do
char in @whitespace ->
tokenize_next(bin, offset + 1)
char == ?% ->
skip_comment(bin, offset + 1)
char == ?< ->
lex_lt(bin, offset)
char == ?> ->
lex_gt(bin, offset)
char == ?[ ->
{:ok, :array_open, offset + 1}
char == ?] ->
{:ok, :array_close, offset + 1}
char == ?/ ->
parse_name(bin, offset + 1, [])
char == ?( ->
parse_literal_string(bin, offset + 1, 1, [])
true ->
parse_word(bin, offset, [])
end
end
defp lex_lt(bin, offset) do
if offset + 1 < byte_size(bin) do
<<next>> = binary_part(bin, offset + 1, 1)
if next == ?<,
do: {:ok, :dict_open, offset + 2},
else: parse_hex_string(bin, offset + 1, [])
else
{:error, :unexpected_eof}
end
end
defp lex_gt(bin, offset) do
if offset + 1 < byte_size(bin) do
<<next>> = binary_part(bin, offset + 1, 1)
if next == ?>, do: {:ok, :dict_close, offset + 2}, else: {:error, :unexpected_gt}
else
{:error, :unexpected_eof}
end
end
defp skip_comment(bin, offset) when offset >= byte_size(bin), do: :eof
defp skip_comment(bin, offset) do
<<char>> = binary_part(bin, offset, 1)
if char == ?\r or char == ?\n,
do: tokenize_next(bin, offset + 1),
else: skip_comment(bin, offset + 1)
end
defp parse_name(bin, offset, acc) when offset >= byte_size(bin) do
{:ok, {:name, finish_name(acc)}, offset}
end
defp parse_name(bin, offset, acc) do
<<char>> = binary_part(bin, offset, 1)
cond do
char <= 32 or char in @delimiters ->
{:ok, {:name, finish_name(acc)}, offset}
# #xx is a 2-hex-digit escape for a name character (ISO 32000 §7.3.5).
char == ?# and hex_escape(bin, offset) != :error ->
{byte, next} = hex_escape(bin, offset)
parse_name(bin, next, [byte | acc])
true ->
parse_name(bin, offset + 1, [char | acc])
end
end
defp hex_escape(bin, offset) do
if offset + 3 <= byte_size(bin) do
<<?#, h1, h2>> = binary_part(bin, offset, 3)
if hex_digit?(h1) and hex_digit?(h2),
do: {hex_value(h1) * 16 + hex_value(h2), offset + 3},
else: :error
else
:error
end
end
defp finish_name(acc), do: :binary.list_to_bin(Enum.reverse(acc))
defp parse_hex_string(bin, offset, _acc) when offset >= byte_size(bin) do
{:error, :unterminated_hex_string}
end
defp parse_hex_string(bin, offset, acc) do
<<char>> = binary_part(bin, offset, 1)
cond do
char == ?> ->
hex = :binary.list_to_bin(Enum.reverse(acc))
normalized = if rem(byte_size(hex), 2) != 0, do: <<hex::binary, ?0>>, else: hex
case Base.decode16(normalized, case: :mixed) do
{:ok, decoded} -> {:ok, {:hex_string, decoded}, offset + 1}
:error -> {:error, :invalid_hex_string}
end
char in @whitespace ->
parse_hex_string(bin, offset + 1, acc)
hex_digit?(char) ->
parse_hex_string(bin, offset + 1, [char | acc])
true ->
{:error, :invalid_hex_char}
end
end
defp hex_digit?(c),
do: (c >= ?0 and c <= ?9) or (c >= ?a and c <= ?f) or (c >= ?A and c <= ?F)
defp hex_value(c) when c >= ?0 and c <= ?9, do: c - ?0
defp hex_value(c) when c >= ?a and c <= ?f, do: c - ?a + 10
defp hex_value(c) when c >= ?A and c <= ?F, do: c - ?A + 10
defp parse_literal_string(_bin, _offset, _depth, _acc) when false, do: :unreachable
defp parse_literal_string(bin, offset, _depth, _acc) when offset >= byte_size(bin) do
{:error, :unterminated_string}
end
defp parse_literal_string(bin, offset, depth, acc) do
<<char>> = binary_part(bin, offset, 1)
case char do
?\\ ->
case decode_escape(bin, offset + 1) do
{:ok, bytes, new_offset} ->
parse_literal_string(bin, new_offset, depth, prepend_reversed(bytes, acc))
{:error, _} = err ->
err
end
?( ->
parse_literal_string(bin, offset + 1, depth + 1, [char | acc])
?) when depth == 1 ->
{:ok, {:literal_string, :binary.list_to_bin(Enum.reverse(acc))}, offset + 1}
?) ->
parse_literal_string(bin, offset + 1, depth - 1, [char | acc])
c ->
parse_literal_string(bin, offset + 1, depth, [c | acc])
end
end
defp decode_escape(bin, offset) when offset >= byte_size(bin), do: {:error, :unexpected_eof}
defp decode_escape(bin, offset) do
<<char>> = binary_part(bin, offset, 1)
case char do
?n ->
{:ok, [?\n], offset + 1}
?r ->
{:ok, [?\r], offset + 1}
?t ->
{:ok, [?\t], offset + 1}
?b ->
{:ok, [?\b], offset + 1}
?f ->
{:ok, [?\f], offset + 1}
?( ->
{:ok, [?(], offset + 1}
?) ->
{:ok, [?)], offset + 1}
?\\ ->
{:ok, [?\\], offset + 1}
?\r ->
new_offset =
if offset + 1 < byte_size(bin) and binary_part(bin, offset + 1, 1) == <<?\n>>,
do: offset + 2,
else: offset + 1
{:ok, [], new_offset}
?\n ->
{:ok, [], offset + 1}
d when d >= ?0 and d <= ?7 ->
parse_octal(bin, offset, 0, 0)
_ ->
{:ok, [char], offset + 1}
end
end
defp parse_octal(bin, offset, acc, count) when count >= 3 or offset >= byte_size(bin) do
{:ok, [rem(acc, 256)], offset}
end
defp parse_octal(bin, offset, acc, count) do
<<char>> = binary_part(bin, offset, 1)
if char >= ?0 and char <= ?7 do
parse_octal(bin, offset + 1, acc * 8 + (char - ?0), count + 1)
else
{:ok, [rem(acc, 256)], offset}
end
end
defp parse_word(bin, offset, acc) when offset >= byte_size(bin) do
categorize_word(:binary.list_to_bin(Enum.reverse(acc)), offset)
end
defp parse_word(bin, offset, acc) do
<<char>> = binary_part(bin, offset, 1)
cond do
acc == [] and (char <= 32 or char in @delimiters) ->
{:error, :unexpected_delimiter}
char <= 32 or char in @delimiters ->
categorize_word(:binary.list_to_bin(Enum.reverse(acc)), offset)
true ->
parse_word(bin, offset + 1, [char | acc])
end
end
defp categorize_word(word, offset) do
case word do
"true" ->
{:ok, {:boolean, true}, offset}
"false" ->
{:ok, {:boolean, false}, offset}
"null" ->
{:ok, :null, offset}
"obj" ->
{:ok, {:keyword, :obj}, offset}
"endobj" ->
{:ok, {:keyword, :endobj}, offset}
"stream" ->
{:ok, {:keyword, :stream}, offset}
"endstream" ->
{:ok, {:keyword, :endstream}, offset}
"xref" ->
{:ok, {:keyword, :xref}, offset}
"trailer" ->
{:ok, {:keyword, :trailer}, offset}
"startxref" ->
{:ok, {:keyword, :startxref}, offset}
"R" ->
{:ok, {:keyword, :R}, offset}
"f" ->
{:ok, {:keyword, :f}, offset}
"n" ->
{:ok, {:keyword, :n}, offset}
_ ->
parse_number(word, offset)
end
end
defp parse_number(word, offset) do
if String.contains?(word, ".") do
case Float.parse(normalize_real(word)) do
{f, ""} -> {:ok, {:number, f}, offset}
_ -> {:ok, {:keyword, safe_keyword(word)}, offset}
end
else
case Integer.parse(word) do
{i, ""} ->
{:ok, {:number, i}, offset}
_ ->
case Float.parse(word) do
{f, ""} -> {:ok, {:number, f}, offset}
_ -> {:ok, {:keyword, safe_keyword(word)}, offset}
end
end
end
end
defp normalize_real("+" <> rest), do: normalize_real(rest)
defp normalize_real("." <> _ = word), do: pad_trailing_dot("0" <> word)
defp normalize_real("-." <> rest), do: pad_trailing_dot("-0." <> rest)
defp normalize_real(word), do: pad_trailing_dot(word)
defp pad_trailing_dot(word),
do: if(String.ends_with?(word, "."), do: word <> "0", else: word)
defp safe_keyword(word) do
String.to_existing_atom(word)
rescue
ArgumentError -> word
end
defp prepend_reversed(bytes, acc), do: Enum.reverse(bytes) ++ acc
end