418 lines
11 KiB
Elixir
418 lines
11 KiB
Elixir
defmodule SnmpKit.SnmpLib.MIB.Compiler do
|
|
@moduledoc """
|
|
Main MIB compiler that orchestrates the entire compilation process.
|
|
|
|
This module provides the main interface for compiling MIB files from source
|
|
to executable format. It coordinates between the lexer, parser, semantic
|
|
analyzer, and code generator to produce optimized compiled MIBs.
|
|
|
|
## Compilation Process
|
|
|
|
1. **Lexical Analysis** - Tokenize MIB source
|
|
2. **Parsing** - Build Abstract Syntax Tree (AST)
|
|
3. **Semantic Analysis** - Validate and resolve symbols
|
|
4. **Code Generation** - Generate optimized runtime format
|
|
5. **Persistence** - Save compiled MIB for loading
|
|
|
|
## Usage
|
|
|
|
# Compile a single MIB file
|
|
{:ok, compiled} = SnmpKit.SnmpLib.MIB.Compiler.compile("MY-MIB.mib")
|
|
|
|
# Compile with options
|
|
{:ok, compiled} = SnmpKit.SnmpLib.MIB.Compiler.compile("MY-MIB.mib",
|
|
output_dir: "/tmp/mibs",
|
|
format: :binary,
|
|
optimize: true
|
|
)
|
|
|
|
# Compile multiple MIBs
|
|
{:ok, results} = SnmpKit.SnmpLib.MIB.Compiler.compile_all([
|
|
"SNMPv2-SMI.mib",
|
|
"MY-MIB.mib"
|
|
])
|
|
"""
|
|
|
|
alias SnmpKit.SnmpLib.MIB.Error
|
|
alias SnmpKit.SnmpLib.MIB.Logger
|
|
alias SnmpKit.SnmpLib.MIB.Parser
|
|
|
|
@type compile_opts :: [
|
|
output_dir: Path.t(),
|
|
format: :erlang | :binary | :json,
|
|
optimize: boolean(),
|
|
validate: boolean(),
|
|
include_paths: [Path.t()],
|
|
warnings_as_errors: boolean()
|
|
]
|
|
|
|
@type compile_result ::
|
|
{:ok, compiled_mib()}
|
|
| {:error, [Error.t()]}
|
|
| {:warning, compiled_mib(), [Error.t()]}
|
|
|
|
@type compiled_mib :: %{
|
|
name: binary(),
|
|
version: binary(),
|
|
format: atom(),
|
|
path: Path.t(),
|
|
metadata: map(),
|
|
oid_tree: SnmpKit.SnmpLib.MIB.AST.oid_tree(),
|
|
symbols: map(),
|
|
dependencies: [binary()]
|
|
}
|
|
|
|
@default_opts [
|
|
output_dir: "./priv/mibs",
|
|
format: :binary,
|
|
optimize: true,
|
|
validate: true,
|
|
include_paths: [],
|
|
warnings_as_errors: false
|
|
]
|
|
|
|
@doc """
|
|
Compile a MIB file from filesystem path.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.MIB.Compiler.compile("test/fixtures/TEST-MIB.mib")
|
|
{:ok, %{name: "TEST-MIB", ...}}
|
|
|
|
iex> SnmpKit.SnmpLib.MIB.Compiler.compile("missing.mib")
|
|
{:error, [%SnmpKit.SnmpLib.MIB.Error{type: :file_not_found}]}
|
|
"""
|
|
@spec compile(Path.t(), compile_opts()) :: compile_result()
|
|
def compile(mib_path, opts \\ []) do
|
|
opts = Keyword.merge(@default_opts, opts)
|
|
|
|
case File.read(mib_path) do
|
|
{:ok, content} ->
|
|
compile_string(content, opts)
|
|
|
|
{:error, reason} ->
|
|
{:error,
|
|
[
|
|
Error.new(:file_not_found,
|
|
message: "Could not read MIB file: #{reason}",
|
|
context: %{path: mib_path}
|
|
)
|
|
]}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Compile a MIB from string content.
|
|
|
|
This delegates to the Parser module which implements the full compilation
|
|
pipeline using YACC-based parsing.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.MIB.Compiler.compile_string(mib_content)
|
|
{:ok, %{name: "TEST-MIB", ...}}
|
|
"""
|
|
@spec compile_string(binary(), compile_opts()) :: compile_result()
|
|
def compile_string(mib_content, opts \\ []) do
|
|
opts = Keyword.merge(@default_opts, opts)
|
|
|
|
# The Parser module implements the full compilation pipeline
|
|
case Parser.parse(mib_content) do
|
|
{:ok, mib} ->
|
|
compile_mib_success(mib, opts)
|
|
|
|
{:error, error} ->
|
|
handle_compile_error(error)
|
|
end
|
|
end
|
|
|
|
defp compile_mib_success(mib, opts) do
|
|
# Convert parser output to compiled_mib format
|
|
compiled = %{
|
|
name: mib.name,
|
|
version: Map.get(mib, :version, "unknown"),
|
|
format: opts[:format],
|
|
# Set by compile/2 if from file
|
|
path: nil,
|
|
metadata: Map.get(mib, :metadata, %{}),
|
|
oid_tree: Map.get(mib, :oid_tree, %{}),
|
|
symbols: build_symbol_table(mib),
|
|
dependencies: extract_dependencies(mib)
|
|
}
|
|
|
|
if opts[:validate] do
|
|
validate_compiled_mib(compiled, opts)
|
|
else
|
|
{:ok, compiled}
|
|
end
|
|
end
|
|
|
|
defp handle_compile_error(error) when is_binary(error) do
|
|
{:error, [Error.new(:syntax_error, message: error)]}
|
|
end
|
|
|
|
defp handle_compile_error(errors) when is_list(errors) do
|
|
{:error, errors}
|
|
end
|
|
|
|
defp handle_compile_error({line, module, message}) do
|
|
# Handle parser errors from YACC
|
|
error_msg =
|
|
case message do
|
|
msg when is_binary(msg) -> msg
|
|
msg when is_list(msg) -> List.to_string(msg)
|
|
msg -> inspect(msg)
|
|
end
|
|
|
|
{:error,
|
|
[
|
|
Error.new(:syntax_error,
|
|
message: "Line #{line}: #{error_msg}",
|
|
line: line,
|
|
context: %{module: module}
|
|
)
|
|
]}
|
|
end
|
|
|
|
defp handle_compile_error(reason) when is_atom(reason) do
|
|
# Handle atom-based errors (e.g., :yecc_not_available, :file_not_found)
|
|
{:error, [Error.new(:compilation_error, message: to_string(reason))]}
|
|
end
|
|
|
|
defp build_symbol_table(mib) do
|
|
# Build a symbol table from the parsed MIB definitions
|
|
Enum.reduce(mib.definitions, %{}, fn def, acc ->
|
|
if Map.has_key?(def, :name) do
|
|
Map.put(acc, def.name, def)
|
|
else
|
|
acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp extract_dependencies(mib) do
|
|
# Extract module dependencies from imports
|
|
mib.imports
|
|
|> Enum.map(& &1.from_module)
|
|
|> Enum.uniq()
|
|
end
|
|
|
|
defp validate_compiled_mib(compiled, _opts) do
|
|
# Note: Semantic validation not yet implemented
|
|
# For now, just return the compiled MIB
|
|
{:ok, compiled}
|
|
end
|
|
|
|
@doc """
|
|
Load a previously compiled MIB.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.MIB.Compiler.load_compiled("priv/mibs/TEST-MIB.mib")
|
|
{:ok, %{name: "TEST-MIB", ...}}
|
|
"""
|
|
@spec load_compiled(Path.t()) :: {:ok, compiled_mib()} | {:error, term()}
|
|
def load_compiled(compiled_path) do
|
|
case File.read(compiled_path) do
|
|
{:ok, binary_data} ->
|
|
# Use safe: true to prevent arbitrary code execution via malicious terms
|
|
case :erlang.binary_to_term(binary_data, [:safe]) do
|
|
%{__type__: :compiled_mib} = compiled ->
|
|
{:ok, compiled}
|
|
|
|
_ ->
|
|
{:error, :invalid_compiled_format}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Compile multiple MIB files in dependency order.
|
|
|
|
## Examples
|
|
|
|
iex> SnmpKit.SnmpLib.MIB.Compiler.compile_all(["SNMPv2-SMI.mib", "MY-MIB.mib"])
|
|
{:ok, [%{name: "SNMPv2-SMI"}, %{name: "MY-MIB"}]}
|
|
"""
|
|
@spec compile_all([Path.t()], compile_opts()) ::
|
|
{:ok, [compiled_mib()]} | {:error, [{Path.t(), [Error.t()]}]}
|
|
def compile_all(mib_files, opts \\ []) do
|
|
opts = Keyword.merge(@default_opts, opts)
|
|
|
|
Logger.log_batch_compilation_start(length(mib_files))
|
|
|
|
# Compile each file independently and guard against crashes so the batch continues
|
|
compile_results =
|
|
Enum.map(mib_files, fn file ->
|
|
try do
|
|
compile(file, opts)
|
|
rescue
|
|
exception ->
|
|
err =
|
|
Error.new(
|
|
:exception,
|
|
message: Exception.message(exception),
|
|
context: %{file: file, exception: exception.__struct__}
|
|
)
|
|
|
|
{:error, [err]}
|
|
catch
|
|
kind, reason ->
|
|
err =
|
|
Error.new(
|
|
:thrown,
|
|
message: "#{inspect(kind)}: #{inspect(reason)}",
|
|
context: %{file: file}
|
|
)
|
|
|
|
{:error, [err]}
|
|
end
|
|
end)
|
|
|
|
{successes, errors} = partition_results(compile_results, mib_files)
|
|
|
|
case errors do
|
|
[] ->
|
|
Logger.log_batch_compilation_success(length(successes))
|
|
{:ok, successes}
|
|
|
|
_ ->
|
|
Logger.log_batch_compilation_error(length(successes), length(errors))
|
|
{:error, errors}
|
|
end
|
|
end
|
|
|
|
# Private helper functions
|
|
|
|
# Note: The following functions are placeholders for future MIB compilation features.
|
|
# They are commented out to avoid Dialyzer warnings until MIB compilation is fully implemented.
|
|
|
|
# defp post_process_mib(mib, opts) do
|
|
# with {:ok, validated_mib} <- validate_mib(mib, opts),
|
|
# {:ok, resolved_mib} <- resolve_symbols(validated_mib, opts),
|
|
# {:ok, optimized_mib} <- optimize_mib(resolved_mib, opts) do
|
|
# {:ok, optimized_mib}
|
|
# end
|
|
# end
|
|
|
|
# defp validate_mib(mib, opts) do
|
|
# if opts[:validate] do
|
|
# # Semantic validation would go here
|
|
# # For now, just basic AST validation
|
|
# case AST.validate_node(mib) do
|
|
# {:ok, _} -> {:ok, mib}
|
|
# {:error, reason} ->
|
|
# error = Error.new(:validation_failed, reason: reason)
|
|
# {:error, [error]}
|
|
# end
|
|
# else
|
|
# {:ok, mib}
|
|
# end
|
|
# end
|
|
|
|
# defp resolve_symbols(mib, _opts) do
|
|
# # Symbol resolution would happen here
|
|
# # For now, return as-is
|
|
# {:ok, mib}
|
|
# end
|
|
|
|
# defp optimize_mib(mib, opts) do
|
|
# if opts[:optimize] do
|
|
# # MIB optimization would happen here
|
|
# # For now, return as-is
|
|
# {:ok, mib}
|
|
# else
|
|
# {:ok, mib}
|
|
# end
|
|
# end
|
|
|
|
# defp build_output_path(mib_name, opts) do
|
|
# output_dir = opts[:output_dir]
|
|
# format = opts[:format]
|
|
#
|
|
# extension = case format do
|
|
# :erlang -> ".erl"
|
|
# :binary -> ".mib"
|
|
# :json -> ".json"
|
|
# end
|
|
#
|
|
# Path.join(output_dir, "#{mib_name}#{extension}")
|
|
# end
|
|
|
|
# defp write_compiled_mib(mib, output_path, opts) do
|
|
# case ensure_output_dir(Path.dirname(output_path)) do
|
|
# :ok ->
|
|
# case opts[:format] do
|
|
# :binary ->
|
|
# compiled = %{
|
|
# __type__: :compiled_mib,
|
|
# name: mib.name,
|
|
# version: get_mib_version(mib),
|
|
# format: :binary,
|
|
# metadata: mib.metadata,
|
|
# oid_tree: mib.oid_tree,
|
|
# symbols: extract_symbols(mib),
|
|
# dependencies: mib.metadata[:dependencies] || [],
|
|
# compiled_at: DateTime.utc_now()
|
|
# }
|
|
# File.write(output_path, :erlang.term_to_binary(compiled))
|
|
#
|
|
# :json ->
|
|
# # Would need JSON serialization
|
|
# {:error, :json_not_implemented}
|
|
#
|
|
# :erlang ->
|
|
# # Would generate Erlang code
|
|
# {:error, :erlang_not_implemented}
|
|
# end
|
|
# error ->
|
|
# error
|
|
# end
|
|
# end
|
|
|
|
# defp ensure_output_dir(dir_path) do
|
|
# case File.mkdir_p(dir_path) do
|
|
# :ok -> :ok
|
|
# {:error, reason} -> {:error, reason}
|
|
# end
|
|
# end
|
|
|
|
# defp get_mib_version(mib) do
|
|
# mib.metadata[:compiler_version] || "unknown"
|
|
# end
|
|
|
|
# defp extract_symbols(mib) do
|
|
# # Build symbol table from definitions
|
|
# Enum.reduce(mib.definitions, %{}, fn definition, acc ->
|
|
# case definition do
|
|
# %{name: name, oid: oid} when is_list(oid) ->
|
|
# Map.put(acc, name, oid)
|
|
# _ ->
|
|
# acc
|
|
# end
|
|
# end)
|
|
# end
|
|
|
|
defp partition_results(results, files) do
|
|
results
|
|
|> Enum.zip(files)
|
|
|> Enum.reduce({[], []}, fn {result, file}, {successes, errors} ->
|
|
case result do
|
|
{:ok, compiled} ->
|
|
{[compiled | successes], errors}
|
|
|
|
{:warning, compiled, _warnings} ->
|
|
{[compiled | successes], errors}
|
|
|
|
{:error, error_list} ->
|
|
{successes, [{file, error_list} | errors]}
|
|
end
|
|
end)
|
|
|> then(fn {successes, errors} ->
|
|
{Enum.reverse(successes), Enum.reverse(errors)}
|
|
end)
|
|
end
|
|
end
|