defmodule BPX.CLI do
@moduledoc """
Command-line interface for BPX compression operations.
Supports packing and unpacking files with automatic compression algorithm selection.
"""
def main(args) do
case parse_args(args) do
{:pack, input_file, output_file, opts} ->
pack_command(input_file, output_file, opts)
{:unpack, input_file, output_file} ->
unpack_command(input_file, output_file)
{:info, input_file} ->
info_command(input_file)
{:help} ->
print_help()
{:error, message} ->
IO.puts(:stderr, "Error: #{message}")
print_help()
System.halt(1)
end
end
defp parse_args(["pack", input_file, output_file | opts]) do
parsed_opts = parse_pack_options(opts, [])
{:pack, input_file, output_file, parsed_opts}
end
defp parse_args(["unpack", input_file, output_file]) do
{:unpack, input_file, output_file}
end
defp parse_args(["info", input_file]) do
{:info, input_file}
end
defp parse_args(["--help"]) do
{:help}
end
defp parse_args(["-h"]) do
{:help}
end
defp parse_args([]) do
{:help}
end
defp parse_args(_) do
{:error, "Invalid arguments"}
end
defp parse_pack_options([], acc), do: acc
defp parse_pack_options(["--algos", algos_str | rest], acc) do
algos =
algos_str
|> String.split(",")
|> Enum.map(&String.to_atom/1)
parse_pack_options(rest, [{:algos, algos} | acc])
end
defp parse_pack_options(["--min-gain", gain_str | rest], acc) do
case Integer.parse(gain_str) do
{gain, ""} -> parse_pack_options(rest, [{:min_gain, gain} | acc])
_ -> [{:error, "Invalid min-gain value: #{gain_str}"}]
end
end
defp parse_pack_options([unknown | _], _acc) do
[{:error, "Unknown option: #{unknown}"}]
end
defp pack_command(input_file, output_file, opts) do
case File.read(input_file) do
{:ok, data} ->
IO.puts("Packing #{input_file} (#{format_bytes(byte_size(data))})...")
algos = Keyword.get(opts, :algos, [:zstd, :brotli, :deflate])
min_gain = Keyword.get(opts, :min_gain, 16)
available_algos = BPX.available_algorithms()
filtered_algos = Enum.filter(algos, &(&1 in available_algos))
final_algos =
if filtered_algos == [] do
IO.puts(:stderr, "Warning: No requested algorithms available, using :deflate")
[:deflate]
else
filtered_algos
end
envelope = BPX.wrap_auto(data, algos: final_algos, min_gain: min_gain)
case File.write(output_file, envelope) do
:ok ->
{:ok, info} = BPX.inspect_envelope(envelope)
IO.puts("✓ Packed successfully!")
IO.puts(" Algorithm: #{info.algorithm}")
IO.puts(" Original: #{format_bytes(info.original_size)}")
IO.puts(" Envelope: #{format_bytes(info.envelope_size)}")
IO.puts(
" Saved: #{format_bytes(info.original_size - info.envelope_size)} (#{Float.round(info.compression_ratio * 100, 1)}%)"
)
{:error, reason} ->
IO.puts(:stderr, "Failed to write output: #{:file.format_error(reason)}")
System.halt(1)
end
{:error, reason} ->
IO.puts(:stderr, "Failed to read input: #{:file.format_error(reason)}")
System.halt(1)
end
end
defp unpack_command(input_file, output_file) do
case File.read(input_file) do
{:ok, envelope} ->
IO.puts("Unpacking #{input_file} (#{format_bytes(byte_size(envelope))})...")
case BPX.unwrap(envelope) do
{:ok, data, meta} ->
case File.write(output_file, data) do
:ok ->
IO.puts("✓ Unpacked successfully!")
IO.puts(" Algorithm: #{meta.algorithm}")
IO.puts(" Envelope: #{format_bytes(byte_size(envelope))}")
IO.puts(" Original: #{format_bytes(meta.original_size)}")
IO.puts(" Restored: #{format_bytes(byte_size(data))}")
{:error, reason} ->
IO.puts(:stderr, "Failed to write output: #{:file.format_error(reason)}")
System.halt(1)
end
{:error, reason} ->
IO.puts(:stderr, "Failed to unpack: #{reason}")
System.halt(1)
end
{:error, reason} ->
IO.puts(:stderr, "Failed to read input: #{:file.format_error(reason)}")
System.halt(1)
end
end
defp info_command(input_file) do
case File.read(input_file) do
{:ok, envelope} ->
case BPX.inspect_envelope(envelope) do
{:ok, info} ->
IO.puts("BPX Envelope Information:")
IO.puts(" File: #{input_file}")
IO.puts(" Algorithm: #{info.algorithm}")
IO.puts(" Original: #{format_bytes(info.original_size)}")
IO.puts(" Compressed: #{format_bytes(info.compressed_size)}")
IO.puts(" Envelope: #{format_bytes(info.envelope_size)}")
IO.puts(" Compression: #{Float.round(info.compression_ratio * 100, 1)}%")
IO.puts(" Overhead: #{info.envelope_size - info.compressed_size} bytes")
{:error, reason} ->
IO.puts(:stderr, "Not a valid BPX file: #{reason}")
System.halt(1)
end
{:error, reason} ->
IO.puts(:stderr, "Failed to read file: #{:file.format_error(reason)}")
System.halt(1)
end
end
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes}B"
defp format_bytes(bytes) when bytes < 1024 * 1024 do
"#{Float.round(bytes / 1024, 1)}KB"
end
defp format_bytes(bytes) when bytes < 1024 * 1024 * 1024 do
"#{Float.round(bytes / (1024 * 1024), 1)}MB"
end
defp format_bytes(bytes) do
"#{Float.round(bytes / (1024 * 1024 * 1024), 1)}GB"
end
defp print_help do
IO.puts("""
BPX CLI - Binary Payload eXchange with automatic compression
Usage:
bpx pack