Removes unreachable catch-all clauses, drops unused `require Logger` lines, adds pin operators to bitstring size patterns, reorders function heads for correct dispatch, and replaces deprecated LoggerBackends calls.
754 lines
23 KiB
Elixir
754 lines
23 KiB
Elixir
defmodule SnmpKit.SnmpLib.MIB.Parser do
|
|
@moduledoc """
|
|
Pure native SNMP MIB parser using custom Elixir grammar.
|
|
|
|
This module provides complete native parsing of SNMP MIB files with 100%
|
|
compatibility. All MIBs are parsed natively ensuring complete data integrity.
|
|
"""
|
|
|
|
alias SnmpKit.SnmpLib.MIB.SnmpTokenizer
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Initialize the parser by compiling the grammar file.
|
|
This creates a proper yacc-generated parser for native MIB parsing.
|
|
"""
|
|
def init_parser do
|
|
module_name = :mib_grammar_elixir
|
|
|
|
# Check if the parser module is already loaded
|
|
case Code.ensure_loaded(module_name) do
|
|
{:module, ^module_name} ->
|
|
# Module is already loaded, no need to recompile
|
|
{:ok, module_name}
|
|
|
|
{:error, :nofile} ->
|
|
# Module not found, need to compile
|
|
compile_grammar()
|
|
end
|
|
end
|
|
|
|
defp compile_grammar do
|
|
# Get the path to our Elixir-compatible grammar file
|
|
grammar_file = Path.join([__DIR__, "..", "..", "..", "src", "mib_grammar_elixir.yrl"])
|
|
|
|
# Ensure the output directory exists
|
|
output_dir = Path.join([__DIR__, "..", "..", "..", "src"])
|
|
File.mkdir_p!(output_dir)
|
|
|
|
# Compile the grammar using Erlang's yecc (if available)
|
|
if Code.ensure_loaded?(:yecc) and function_exported?(:yecc, :file, 1) do
|
|
# credo:disable-for-next-line Credo.Check.Refactor.Apply
|
|
result = apply(:yecc, :file, [to_charlist(grammar_file)])
|
|
|
|
case result do
|
|
{:ok, _generated_file} ->
|
|
module_name = :mib_grammar_elixir
|
|
|
|
Logger.debug("Successfully compiled MIB grammar. Generated module: #{inspect(module_name)}")
|
|
|
|
{:ok, module_name}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to compile MIB grammar: #{inspect(reason)}")
|
|
{:error, {:grammar_compilation_failed, reason}}
|
|
|
|
:error ->
|
|
Logger.error("Grammar compilation failed with error")
|
|
{:error, :compilation_failed}
|
|
end
|
|
else
|
|
Logger.warning("yecc module not available, MIB grammar compilation disabled")
|
|
{:error, :yecc_not_available}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Parse all MIB files in a list of directories using pure native parsing.
|
|
|
|
Returns a map with directory paths as keys and results as values.
|
|
Each result contains successful compilations and failures.
|
|
|
|
## Examples
|
|
|
|
# Parse MIBs in multiple directories
|
|
dirs = [
|
|
"/path/to/mibs/working",
|
|
"/path/to/mibs/docsis"
|
|
]
|
|
results = SnmpKit.SnmpLib.MIB.Parser.mibdirs(dirs)
|
|
|
|
# Access results by directory
|
|
working_results = results["/path/to/mibs/working"]
|
|
IO.puts("Success: \#{length(working_results.success)}/\#{working_results.total}")
|
|
|
|
# Get all successful MIBs across directories
|
|
all_mibs = Enum.flat_map(results, fn {_dir, result} -> result.success end)
|
|
"""
|
|
def mibdirs(directories) when is_list(directories) do
|
|
Logger.info("Compiling MIBs in #{length(directories)} directories")
|
|
|
|
results =
|
|
Map.new(directories, fn dir ->
|
|
{dir, compile_directory(dir)}
|
|
end)
|
|
|
|
# Log summary
|
|
total_success =
|
|
Enum.reduce(results, 0, fn {_dir, result}, acc ->
|
|
acc + length(result.success)
|
|
end)
|
|
|
|
total_files =
|
|
Enum.reduce(results, 0, fn {_dir, result}, acc ->
|
|
acc + result.total
|
|
end)
|
|
|
|
Logger.info(
|
|
"OVERALL RESULTS: Total MIBs compiled: #{total_success}/#{total_files} (#{Float.round(total_success / max(total_files, 1) * 100, 1)}%)"
|
|
)
|
|
|
|
Enum.each(results, fn {dir, result} ->
|
|
dir_name = Path.basename(dir)
|
|
success_rate = Float.round(length(result.success) / max(result.total, 1) * 100, 1)
|
|
Logger.info(" #{dir_name}: #{length(result.success)}/#{result.total} (#{success_rate}%)")
|
|
end)
|
|
|
|
results
|
|
end
|
|
|
|
@doc """
|
|
Parse a MIB file using pure native grammar parsing.
|
|
This is the production MIB parser with 100% native compatibility.
|
|
"""
|
|
def parse(mib_content) when is_binary(mib_content) do
|
|
with {:ok, parser_module} <- init_parser(),
|
|
{:ok, tokens} <- tokenize(mib_content),
|
|
{:ok, parse_tree} <- parse_with_module(parser_module, tokens) do
|
|
{:ok, convert_to_elixir_format(parse_tree)}
|
|
else
|
|
{:error, {:tokenize, reason}} ->
|
|
Logger.debug("Tokenize failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
|
|
{:error, {:parse, reason}} ->
|
|
Logger.debug("Parse failed: #{inspect(reason)}")
|
|
{:error, convert_error_to_string(reason)}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_with_module(parser_module, tokens) do
|
|
# credo:disable-for-next-line Credo.Check.Refactor.Apply
|
|
case apply(parser_module, :parse, [tokens]) do
|
|
{:ok, parse_tree} -> {:ok, parse_tree}
|
|
{:error, reason} -> {:error, {:parse, reason}}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Parse pre-tokenized MIB tokens using native grammar parsing.
|
|
This function takes tokens directly without tokenizing.
|
|
"""
|
|
def parse_tokens(tokens) when is_list(tokens) do
|
|
with {:ok, parser_module} <- init_parser(),
|
|
{:ok, parse_tree} <- parse_with_module(parser_module, tokens) do
|
|
{:ok, convert_to_elixir_format(parse_tree)}
|
|
else
|
|
{:error, {:parse, reason}} ->
|
|
Logger.debug("Parse failed: #{inspect(reason)}")
|
|
{:error, convert_error_to_string(reason)}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Tokenize MIB content using the native SNMP tokenizer.
|
|
|
|
Uses the SnmpKit.SnmpLib.MIB.SnmpTokenizer module for complete MIB tokenization.
|
|
"""
|
|
def tokenize(mib_content) when is_binary(mib_content) do
|
|
# Convert to charlist for Erlang compatibility
|
|
char_content = to_charlist(mib_content)
|
|
|
|
# Use the native SNMP tokenizer
|
|
case SnmpTokenizer.tokenize(
|
|
char_content,
|
|
&SnmpTokenizer.null_get_line/0
|
|
) do
|
|
{:ok, tokens} ->
|
|
Logger.debug("Tokenized MIB content successfully")
|
|
# Apply hex atom conversion to tokens from the tokenizer
|
|
converted_tokens = apply_hex_conversion(tokens)
|
|
{:ok, converted_tokens}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Tokenization failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
# Apply hex atom conversion to tokens from the tokenizer.
|
|
# Converts long hex atoms like :"07fffffff" to integers for grammar compatibility.
|
|
defp apply_hex_conversion(tokens) do
|
|
Enum.map(tokens, &convert_hex_atom/1)
|
|
end
|
|
|
|
# Convert hex atoms that look like integers to actual integers
|
|
defp convert_hex_atom({:atom, line, atom_value}) when is_atom(atom_value) do
|
|
atom_string = Atom.to_string(atom_value)
|
|
|
|
# Check if it looks like a hex number (only convert long hex strings, not short identifiers like d1, d2)
|
|
if String.match?(atom_string, ~r/^[0-9a-fA-F]{8,}$/) do
|
|
try do
|
|
# Try to convert from hex to integer
|
|
hex_value = String.to_integer(atom_string, 16)
|
|
{:integer, line, hex_value}
|
|
rescue
|
|
_ ->
|
|
# If conversion fails, keep as atom
|
|
{:atom, line, atom_value}
|
|
end
|
|
else
|
|
# Not a hex pattern, keep as atom
|
|
{:atom, line, atom_value}
|
|
end
|
|
end
|
|
|
|
# Pass through all other tokens unchanged
|
|
defp convert_hex_atom(token), do: token
|
|
|
|
# Convert the Erlang parse tree to Elixir-friendly format.
|
|
defp convert_to_elixir_format(result) do
|
|
case result do
|
|
{:pdata, version, mib_name, exports, imports, definitions} ->
|
|
%{
|
|
__type__: :mib,
|
|
name: to_string(mib_name),
|
|
version: version,
|
|
exports: convert_exports(exports),
|
|
imports: convert_imports(imports),
|
|
definitions: convert_definitions(definitions)
|
|
}
|
|
|
|
{:pdata, version, mib_name, imports, definitions} ->
|
|
%{
|
|
__type__: :mib,
|
|
name: to_string(mib_name),
|
|
version: version,
|
|
exports: [],
|
|
imports: convert_imports(imports),
|
|
definitions: convert_definitions(definitions)
|
|
}
|
|
|
|
other ->
|
|
%{__type__: :unknown, raw: other}
|
|
end
|
|
end
|
|
|
|
defp convert_exports(exports) when is_list(exports) do
|
|
Enum.map(exports, fn export ->
|
|
case export do
|
|
{type, name} -> %{type: type, name: to_string(name)}
|
|
name -> %{name: to_string(name)}
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp convert_imports(imports) when is_list(imports) do
|
|
Enum.map(imports, fn
|
|
{{module_name, symbols}, _line} ->
|
|
%{
|
|
__type__: :import,
|
|
from_module: to_string(module_name),
|
|
symbols:
|
|
Enum.map(symbols, fn
|
|
{:builtin, symbol} -> to_string(symbol)
|
|
{:node, symbol} -> to_string(symbol)
|
|
{:type, symbol} -> to_string(symbol)
|
|
symbol -> to_string(symbol)
|
|
end)
|
|
}
|
|
|
|
{{module_name, symbols}} ->
|
|
%{
|
|
__type__: :import,
|
|
from_module: to_string(module_name),
|
|
symbols:
|
|
Enum.map(symbols, fn
|
|
{:builtin, symbol} -> to_string(symbol)
|
|
{:node, symbol} -> to_string(symbol)
|
|
{:type, symbol} -> to_string(symbol)
|
|
symbol -> to_string(symbol)
|
|
end)
|
|
}
|
|
|
|
other ->
|
|
%{__type__: :import, raw: other}
|
|
end)
|
|
end
|
|
|
|
defp convert_imports(_imports) do
|
|
[]
|
|
end
|
|
|
|
defp convert_definitions(definitions) when is_list(definitions) do
|
|
Enum.map(definitions, &convert_definition/1)
|
|
end
|
|
|
|
# Handle the actual Erlang SNMP record format from the grammar
|
|
defp convert_definition({{record_type, name, macro, parent, sub_index}, line}) when record_type == :mc_internal do
|
|
%{
|
|
__type__: :object_identifier,
|
|
name: to_string(name),
|
|
macro: macro,
|
|
parent: if(is_binary(parent), do: parent, else: to_string(parent)),
|
|
sub_index: convert_sub_index(sub_index),
|
|
line: line
|
|
}
|
|
end
|
|
|
|
defp convert_definition({{record_type, name, syntax, units, max_acc, status, desc, ref, kind, oid}, line})
|
|
when record_type == :mc_object_type do
|
|
%{
|
|
__type__: :object_type,
|
|
name: to_string(name),
|
|
syntax: syntax,
|
|
units: units,
|
|
max_access: max_acc,
|
|
status: status,
|
|
description: clean_description(desc),
|
|
reference: ref,
|
|
kind: kind,
|
|
oid: convert_oid(oid),
|
|
line: line
|
|
}
|
|
end
|
|
|
|
# Handle module identity record
|
|
defp convert_definition(
|
|
{{:mc_module_identity, name, last_updated, organization, contact_info, description, revisions, oid}, line}
|
|
) do
|
|
%{
|
|
__type__: :module_identity,
|
|
name: to_string(name),
|
|
last_updated: to_string(last_updated),
|
|
organization: clean_description(to_string(organization)),
|
|
contact_info: clean_description(to_string(contact_info)),
|
|
description: clean_description(to_string(description)),
|
|
revisions: convert_revisions(revisions),
|
|
oid: convert_oid(oid),
|
|
line: line
|
|
}
|
|
end
|
|
|
|
# Handle textual convention record
|
|
defp convert_definition({{:mc_new_type, name, macro, status, description, reference, display_hint, syntax}, line}) do
|
|
%{
|
|
__type__: :textual_convention,
|
|
name: to_string(name),
|
|
macro: macro,
|
|
status: status,
|
|
description: clean_description(to_string(description)),
|
|
reference: if(reference == :undefined, do: nil, else: to_string(reference)),
|
|
display_hint: if(display_hint == :undefined, do: nil, else: to_string(display_hint)),
|
|
syntax: syntax,
|
|
line: line
|
|
}
|
|
end
|
|
|
|
# Handle legacy format first (more specific pattern)
|
|
defp convert_definition({:ok, {type, name, rest}}) do
|
|
base = %{
|
|
__type__: type,
|
|
name: to_string(name)
|
|
}
|
|
|
|
case type do
|
|
:objectidentifier ->
|
|
Map.put(base, :oid, convert_oid(rest))
|
|
|
|
:objectType ->
|
|
convert_object_type(base, rest)
|
|
|
|
:moduleIdentity ->
|
|
convert_module_identity(base, rest)
|
|
|
|
:textualConvention ->
|
|
convert_textual_convention(base, rest)
|
|
|
|
:objectGroup ->
|
|
convert_object_group(base, rest)
|
|
|
|
_ ->
|
|
Map.put(base, :data, rest)
|
|
end
|
|
end
|
|
|
|
# Handle other record types as catch-all
|
|
defp convert_definition({record_tuple, line}) do
|
|
%{
|
|
__type__: :unknown,
|
|
record: record_tuple,
|
|
line: line
|
|
}
|
|
end
|
|
|
|
defp convert_object_type(base, {syntax, access, status, description, reference, index, defval, oid}) do
|
|
base
|
|
|> Map.put(:syntax, convert_syntax(syntax))
|
|
|> Map.put(:max_access, convert_atom(access))
|
|
|> Map.put(:status, convert_atom(status))
|
|
|> Map.put(:description, clean_description(to_string(description)))
|
|
|> Map.put(:reference, if(reference == :undefined, do: nil, else: to_string(reference)))
|
|
|> Map.put(:index, convert_index(index))
|
|
|> Map.put(:defval, convert_defval(defval))
|
|
|> Map.put(:oid, convert_oid(oid))
|
|
end
|
|
|
|
defp convert_module_identity(base, {last_updated, organization, contact_info, description, revisions, oid}) do
|
|
base
|
|
|> Map.put(:last_updated, to_string(last_updated))
|
|
|> Map.put(:organization, to_string(organization))
|
|
|> Map.put(:contact_info, to_string(contact_info))
|
|
|> Map.put(:description, clean_description(to_string(description)))
|
|
|> Map.put(:revisions, convert_revisions(revisions))
|
|
|> Map.put(:oid, convert_oid(oid))
|
|
end
|
|
|
|
defp convert_textual_convention(base, {display_hint, status, description, reference, syntax}) do
|
|
base
|
|
|> Map.put(
|
|
:display_hint,
|
|
if(display_hint == :undefined, do: nil, else: to_string(display_hint))
|
|
)
|
|
|> Map.put(:status, convert_atom(status))
|
|
|> Map.put(:description, clean_description(to_string(description)))
|
|
|> Map.put(:reference, if(reference == :undefined, do: nil, else: to_string(reference)))
|
|
|> Map.put(:syntax, convert_syntax(syntax))
|
|
end
|
|
|
|
defp convert_object_group(base, {objects, status, description, reference, oid}) do
|
|
base
|
|
|> Map.put(:objects, Enum.map(objects, &to_string/1))
|
|
|> Map.put(:status, convert_atom(status))
|
|
|> Map.put(:description, clean_description(to_string(description)))
|
|
|> Map.put(:reference, if(reference == :undefined, do: nil, else: to_string(reference)))
|
|
|> Map.put(:oid, convert_oid(oid))
|
|
end
|
|
|
|
defp convert_oid(oid_list) when is_list(oid_list) do
|
|
Enum.map(oid_list, fn
|
|
{name, value} when is_atom(name) and is_integer(value) ->
|
|
%{name: to_string(name), value: value}
|
|
|
|
{name, value} when is_atom(name) and is_list(value) ->
|
|
# Handle charlists in tuple values
|
|
%{name: to_string(name), value: convert_oid_value(value)}
|
|
|
|
value when is_integer(value) ->
|
|
%{value: value}
|
|
|
|
value when is_list(value) ->
|
|
# Handle charlists
|
|
%{value: convert_oid_value(value)}
|
|
|
|
name when is_atom(name) ->
|
|
%{name: to_string(name)}
|
|
end)
|
|
end
|
|
|
|
# Handle tuple OIDs like {:"mib-2", ~c"4"}
|
|
defp convert_oid({name, value}) when is_atom(name) do
|
|
{name, convert_oid_value(value)}
|
|
end
|
|
|
|
# Handle other OID formats
|
|
defp convert_oid(oid), do: oid
|
|
|
|
# Convert OID values, handling charlists
|
|
defp convert_oid_value(value) when is_list(value) do
|
|
# Check if it's a charlist that can be converted to string
|
|
if Enum.all?(value, fn
|
|
i when is_integer(i) -> i >= 0 and i <= 1_114_111
|
|
_ -> false
|
|
end) do
|
|
# Convert charlist to string, then try to parse as integer if possible
|
|
str_value = List.to_string(value)
|
|
|
|
case Integer.parse(str_value) do
|
|
# Pure integer string
|
|
{int_value, ""} -> int_value
|
|
# Keep as string if not pure integer
|
|
_ -> str_value
|
|
end
|
|
else
|
|
# Not a charlist, return as-is
|
|
value
|
|
end
|
|
rescue
|
|
# If conversion fails, return original
|
|
_ -> value
|
|
end
|
|
|
|
defp convert_oid_value(value), do: value
|
|
|
|
defp convert_syntax(:integer), do: :integer
|
|
defp convert_syntax({:integer, constraints}), do: {:integer, convert_constraints(constraints)}
|
|
defp convert_syntax(:"octet string"), do: :octet_string
|
|
defp convert_syntax({:"octet string", size}), do: {:octet_string, convert_constraints(size)}
|
|
defp convert_syntax(:"object identifier"), do: :object_identifier
|
|
defp convert_syntax(atom) when is_atom(atom), do: atom
|
|
|
|
defp convert_constraints(constraints), do: constraints
|
|
|
|
defp convert_index(:undefined), do: nil
|
|
|
|
defp convert_index(index_list) when is_list(index_list) do
|
|
Enum.map(index_list, fn
|
|
{:implied, name} -> {:implied, to_string(name)}
|
|
name when is_atom(name) -> to_string(name)
|
|
end)
|
|
end
|
|
|
|
defp convert_defval(:undefined), do: nil
|
|
defp convert_defval(value), do: value
|
|
|
|
defp convert_revisions(revisions) when is_list(revisions) do
|
|
Enum.map(revisions, fn
|
|
{:mc_revision, date, description} ->
|
|
%{date: to_string(date), description: clean_description(to_string(description))}
|
|
|
|
{date, description} ->
|
|
%{date: to_string(date), description: clean_description(to_string(description))}
|
|
end)
|
|
end
|
|
|
|
defp convert_revisions(_revisions), do: []
|
|
|
|
defp convert_atom(atom) when is_atom(atom) do
|
|
atom |> to_string() |> String.replace("-", "_") |> String.to_atom()
|
|
end
|
|
|
|
# Helper function to compile all MIB files in a directory
|
|
defp compile_directory(directory) do
|
|
case File.ls(directory) do
|
|
{:ok, files} ->
|
|
mib_files =
|
|
files
|
|
|> Enum.filter(&mib_file?/1)
|
|
|> Enum.sort()
|
|
|
|
Logger.debug("Processing #{Path.basename(directory)}: #{length(mib_files)} files")
|
|
|
|
results = Enum.map(mib_files, &process_mib_file(directory, &1))
|
|
|
|
successes = Enum.filter(results, &(elem(&1, 0) == :success))
|
|
failures = Enum.filter(results, &(elem(&1, 0) == :error))
|
|
|
|
# Extract MIB data from successes
|
|
success_mibs =
|
|
Enum.map(successes, fn {:success, file, mib_data} ->
|
|
Map.put(mib_data, :source_file, file)
|
|
end)
|
|
|
|
# Extract error info from failures
|
|
error_info =
|
|
Enum.map(failures, fn {:error, file, reason} ->
|
|
%{file: file, error: reason}
|
|
end)
|
|
|
|
%{
|
|
directory: directory,
|
|
total: length(mib_files),
|
|
success: success_mibs,
|
|
failures: error_info,
|
|
success_count: length(successes),
|
|
failure_count: length(failures)
|
|
}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Cannot read directory #{directory}: #{inspect(reason)}")
|
|
|
|
%{
|
|
directory: directory,
|
|
total: 0,
|
|
success: [],
|
|
failures: [%{file: "directory", error: reason}],
|
|
success_count: 0,
|
|
failure_count: 1
|
|
}
|
|
end
|
|
end
|
|
|
|
# Process a single MIB file
|
|
defp process_mib_file(directory, file) do
|
|
file_path = Path.join(directory, file)
|
|
|
|
with {:ok, content} <- File.read(file_path),
|
|
{:ok, mib_data} <- parse(content) do
|
|
{:success, file, mib_data}
|
|
else
|
|
{:error, {:file_read_error, _}} = error ->
|
|
error
|
|
|
|
{:error, reason} ->
|
|
{:error, file, reason}
|
|
end
|
|
end
|
|
|
|
# Helper to identify MIB files
|
|
defp mib_file?(filename) do
|
|
String.ends_with?(filename, ".mib") or
|
|
(not String.contains?(filename, ".") and not String.ends_with?(filename, ".bin"))
|
|
end
|
|
|
|
# Clean up description strings by trimming lines and removing excessive whitespace
|
|
defp clean_description(desc) when is_binary(desc) do
|
|
# Early exit for very large descriptions to prevent timeout
|
|
if byte_size(desc) > 50_000 do
|
|
# For very large descriptions, just do basic cleanup
|
|
desc
|
|
|> String.trim()
|
|
# Truncate to reasonable size
|
|
|> String.slice(0, 1000)
|
|
|> Kernel.<>(" [truncated]")
|
|
else
|
|
desc
|
|
|> String.split("\n")
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.reject(&(&1 == ""))
|
|
|> Enum.join(" ")
|
|
|> collapse_whitespace()
|
|
|> String.trim()
|
|
end
|
|
end
|
|
|
|
defp clean_description(desc), do: desc
|
|
|
|
# Efficiently collapse multiple whitespace characters into single spaces
|
|
defp collapse_whitespace(text) do
|
|
text
|
|
|> String.split()
|
|
|> Enum.join(" ")
|
|
end
|
|
|
|
# Convert charlist error messages to binary strings
|
|
defp convert_error_to_string({mib_name, module, message}) when is_binary(mib_name) and is_atom(module) do
|
|
# Handle Erlang parser errors like {"MIB-NAME", :parser_module, "error message"}
|
|
message_str =
|
|
if is_list(message), do: convert_deep_charlist(message), else: to_string(message)
|
|
|
|
"#{mib_name}: #{message_str}"
|
|
end
|
|
|
|
defp convert_error_to_string({line, module, message}) when is_list(message) do
|
|
{line, module, convert_deep_charlist(message)}
|
|
end
|
|
|
|
defp convert_error_to_string(message), do: message
|
|
|
|
defp convert_deep_charlist(list) when is_list(list) do
|
|
# Handle lists of charlists like [[115, 121, 110, ...], [39, 84, 69, ...]]
|
|
if list_of_charlists?(list) do
|
|
Enum.map_join(list, "", &charlist_to_string/1)
|
|
else
|
|
# Try to convert as a single charlist
|
|
charlist_to_string(list)
|
|
end
|
|
rescue
|
|
# If conversion fails, return original
|
|
_ -> list
|
|
end
|
|
|
|
defp list_of_charlists?(list) do
|
|
Enum.all?(list, fn
|
|
sublist when is_list(sublist) -> charlist?(sublist)
|
|
_ -> false
|
|
end)
|
|
end
|
|
|
|
defp charlist?(list) do
|
|
Enum.all?(list, fn
|
|
i when is_integer(i) -> i >= 0 and i <= 1_114_111
|
|
# Allow mixed content for improper lists like [115, 121 | ""]
|
|
_ -> true
|
|
end)
|
|
rescue
|
|
_ -> false
|
|
end
|
|
|
|
defp charlist_to_string(charlist) when is_list(charlist) do
|
|
# Handle improper lists like [115, 121, 110 | ""]
|
|
case charlist do
|
|
[] ->
|
|
""
|
|
|
|
[head | _tail] when is_integer(head) and head >= 0 and head <= 1_114_111 ->
|
|
try do
|
|
# Try to convert the whole thing, handling improper lists
|
|
convert_improper_charlist(charlist, [])
|
|
rescue
|
|
# Fallback to inspect if conversion fails
|
|
_ -> inspect(charlist)
|
|
end
|
|
|
|
_ ->
|
|
inspect(charlist)
|
|
end
|
|
rescue
|
|
_ -> inspect(charlist)
|
|
end
|
|
|
|
defp charlist_to_string(other), do: inspect(other)
|
|
|
|
defp convert_improper_charlist([], acc) do
|
|
acc |> Enum.reverse() |> List.to_string()
|
|
end
|
|
|
|
defp convert_improper_charlist([head | tail], acc) when is_integer(head) do
|
|
convert_improper_charlist(tail, [head | acc])
|
|
end
|
|
|
|
defp convert_improper_charlist(other, acc) when is_binary(other) do
|
|
# Handle case where tail is a string (like in [115, 121 | ""])
|
|
(acc |> Enum.reverse() |> List.to_string()) <> other
|
|
end
|
|
|
|
defp convert_improper_charlist(_, acc) do
|
|
# For any other tail, just convert what we have
|
|
acc |> Enum.reverse() |> List.to_string()
|
|
end
|
|
|
|
# Convert sub_index from charlist to appropriate format
|
|
defp convert_sub_index(sub_index) when is_list(sub_index) do
|
|
# Check if it's a charlist that can be converted to string
|
|
if Enum.all?(sub_index, fn
|
|
i when is_integer(i) -> i >= 0 and i <= 1_114_111
|
|
_ -> false
|
|
end) do
|
|
case List.to_string(sub_index) do
|
|
# Handle common cases
|
|
# Convert newline to nil (often used as placeholder)
|
|
"\n" -> nil
|
|
# Convert empty string to nil
|
|
"" -> nil
|
|
# Keep as string
|
|
str -> str
|
|
end
|
|
else
|
|
# If not a pure charlist, return as-is (might be list of integers)
|
|
sub_index
|
|
end
|
|
rescue
|
|
# If conversion fails, return original
|
|
_ -> sub_index
|
|
end
|
|
|
|
defp convert_sub_index(sub_index), do: sub_index
|
|
end
|